Перейти к содержанию

Streaming API и Prefetch

src/torrent/api.ts — единственная точка входа фронтенда для запуска аудио stream-ов. Он обрабатывает оба пути — RuTracker (vozduxan/libtorrent) и SoulSeek, управляет cache файлов .torrent и координирует prefetch следующего трека.

Источники: src/torrent/api.ts, src/torrent/torrentSession.ts


streamUrl(magnet, fileIdx, opts)

Главная функция, вызываемая плеером, когда трек нужно начать воспроизводить.

export interface StreamUrlOpts {
  source?: string;           // "rutracker" | "soulseek"
  torrentId?: string | number;
  slskUsername?: string;
  slskFilepath?: string;
  slskFilesize?: number;
}

Путь SoulSeek

if (src === "soulseek") {
  const ready = await soulseekPrepareStream(slskUsername, slskFilepath, slskFilesize ?? 0);
  return ready?.url ?? "";
}

Вызывает soulseek_prepare_stream (Tauri invoke), возвращает http://127.0.0.1:<port>/slsk/<token>.

Путь RuTracker / vozduxan

const m = enrichMagnetWithOpenTrackers(magnet);
let torrentFileB64 = null;
if (src === "rutracker" && tid !== "") {
  torrentFileB64 = await _cachedTorrentFileB64(tid);
}
const ready = await invoke("torrent_prepare_stream", { magnet: m, fileIdx, torrentFileB64 });
return ready?.url ?? "";

enrichMagnetWithOpenTrackers добавляет набор URL открытых трекеров к magnet URI. Это позволяет избежать задержки поиска только через DHT для свежих magnet-ссылок.

torrentFileB64 — содержимое файла .torrent в кодировке base64. Его передача в torrent_prepare_stream позволяет пропустить ожидание metadata_received_alert в libtorrent — neegde может начать буферизацию сразу, не дожидаясь обмена метаданными через DHT. Для topic-ов RuTracker бинарник скачивается с dl.php (см. Torrent Details).


Cache файлов .torrent: _cachedTorrentFileB64

Cache позволяет не перекачивать файлы .torrent для треков из одного торрента, которые пользователь последовательно воспроизводит.

const _torrentFileCache = new Map<string, Promise<string | null>>();
const TORRENT_FILE_CACHE_MAX = 30;
const _TORRENT_LS_PREFIX = "torrent_file_b64_v1_";

Cache — это Map<topicId, Promise<b64string>>. Хранение promise-ов (а не разрешённых значений) означает, что если два трека из одного торрента начинают стриминг одновременно, инициируется только одна загрузка — оба вызывающих ожидают одного и того же promise.

Сохранение: записи также записываются в localStorage (префикс ключа torrent_file_b64_v1_<topicId>). При инициализации модуля _loadPersistedTorrentFiles читает их обратно, чтобы cache сохранялся между перезагрузками страницы и перезапусками приложения без повторных запросов к dl.php.

LRU-вытеснение: когда in-memory cache превышает 30 записей, самая старая (первая в порядке вставки в Map) удаляется. Когда localStorage превышает 30 записей, самые старые ключи удаляются.


prefetchNextInQueue(current, next, opts)

Вызывается плеером, когда прогресс воспроизведения достигает ~22% или после 30 секунд (в зависимости от того, что наступит раньше). Предварительно прогревает окно приоритетов кусков vozduxan для следующего трека, прежде чем пользователь до него доберётся.

const result = await invoke("torrent_prefetch_next_track", {
  currentMagnet: m0,
  currentFileIdx: current.fileIdx,
  nextMagnet: m1,
  nextFileIdx: next.fileIdx,
  nextTorrentFileB64,   // null для SoulSeek (концепция torrent-файла отсутствует)
  warmOnly,             // true = использовать warm_prefetch_token (слот track+2)
});

warmOnly управляет тем, какой token bucket использует сторона Rust: - falseprefetch_token (track+1, реальный следующий трек) - truewarm_prefetch_token (track+2, прогрев второго по счёту трека)

Оба вызова исходят из usePrefetch.ts, который отслеживает позицию в очереди и запускает: 1. prefetchNextInQueue(current, next, { warmOnly: false }) — на 22% или 30с 2. prefetchNextInQueue(current, secondNext, { warmOnly: true }) — на более позднем пороге

Разделение по warmOnly гарантирует, что прогрев второго по счёту трека никогда не вытеснит уже прогретый реальный следующий трек (см. token bucket-ы обёртки Rust).


torrentFileB64ForTrack(track)

Вспомогательная функция, используемая prefetchNextInQueue для загрузки файла .torrent следующего трека перед вызовом Tauri-команды. Таким образом, payload в base64 включается в команду prefetch, что позволяет также избежать ожидания метаданных в libtorrent для prefetch-торрента.

Срабатывает только для источников RuTracker с непустым torrentId. Для треков SoulSeek сразу возвращает null.


releaseTorrentStreamUrl(token)

В src/torrent/torrentSession.ts:

export async function releaseTorrentStreamUrl(token: string): Promise<void> {
  await invoke("torrent_release_stream", { token });
}

Вызывается плеером, когда трек заканчивается, пропускается или компонент размонтируется. Освобождает stream token vozduxan, что запускает ожидание потока приоритетов (~100 мс) в C++ и помечает stream как idle.


Логирование отладки

Все вызовы streamUrl и prefetchNextInQueue логируются в консоль отладки приложения через appDebugLog("stream", msg). Ключевые точки логирования:

  • torrent file b64: cache hit / downloading from rutracker — показывает, использовался ли cache .torrent
  • streamUrl: invoking prepare — до вызова Tauri, показывает fileIdx и наличие torrent-файла
  • streamUrl: prepare OK / FAILED — результат с затраченным временем
  • prefetchNextInQueue: start — показывает warm vs next, fileIdx, torrentId
  • prefetchNextInQueue: OK / FAILED — вид результата и затраченное время