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

Стриминг SoulSeek

Потоковое воспроизведение аудио через SoulSeek следует той же схеме, что и путь через vozduxan, с точки зрения фронтенда: streamUrl() возвращает локальный HTTP URL, и элемент <audio> воспроизводит с него. Разница — целиком на стороне бэкенда.

См. также: Передача файлов — полный протокол P2P-скачивания.


Полный поток воспроизведения

Пользователь кликает на трек SoulSeek
  ▼ streamUrl(magnet, fileIdx, { source: "soulseek", slskUsername, slskFilepath, slskFilesize })
  ▼ soulseekPrepareStream(username, filepath, filesize)
  │   ≡ invoke("soulseek_prepare_stream", { username, filepath, filesize })
  ▼ Rust: download_and_stream(session, username, filepath, filesize, token)
  │   │
  │   ├── современный путь: QueueUpload → TransferRequest → TransferResponse → F connection
  │   │   (при неудаче откатывается к устаревшему TransferRequest)
  │   │
  │   ├── download_loop: читает из F socket → neegde_slsk_<token>.tmp
  │   │
  │   └── http_server_loop на 127.0.0.1:<random_port>
  ▼ Возвращает { url: "http://127.0.0.1:<port>/slsk/<token>", token: "<n>" }
  ▼ <audio src={url} /> начинает воспроизведение
  │   HTTP Range-запросы → http_server_loop → читает temp-файл, опрашивает счётчик downloaded
  ▼ Трек завершился / пользователь пропустил
      invoke("soulseek_release_stream", { token })
      → прерывает задачу загрузки + HTTP-задачу
      → удаляет neegde_slsk_<token>.tmp

Ключевые отличия от пути через vozduxan

Аспект vozduxan (RuTracker) SoulSeek
Источник загрузки libtorrent через DHT/трекеры Одноранговый P2P TCP
Локальный сервер C++ HTTP-сервер (vozduxan) Rust HTTP-сервер (transfer.rs)
Stream URL http://127.0.0.1:<port>/stream/<token> http://127.0.0.1:<port>/slsk/<token>
Поддержка перемотки Полная (окно приоритетов кусков сдвигается) Полная (Range-запросы, опрос temp-файла)
Prefetch Token buckets (current/next/warm) Не реализован — треки SoulSeek однораздаточные
Release torrent_release_stream → ожидание C++ soulseek_release_stream → прерывание + удаление файла
Тип token Строка (vozduxan UUID) Строка (u32 как строка)

Команда soulseek_prepare_stream

const ready = await invoke<{ url: string; token: string }>(
  "soulseek_prepare_stream",
  { username, filepath, filesize }
);
// ready.url = "http://127.0.0.1:<port>/slsk/<token>"
// ready.token = "<u32 as string>"

Rust-обработчик:

  1. Получает активную Session (возвращает ошибку, если отключён или session мертва).
  2. Генерирует новый token = next_token().
  3. Вызывает transfer::download_and_stream(session, username, filepath, filesize, token).
  4. Сохраняет ActiveStream { temp_path, download_abort, http_abort } в SoulSeekState.streams с ключом token.to_string().
  5. Возвращает { url, token }.

Команда soulseek_release_stream

await invoke("soulseek_release_stream", { token: string });

Находит ActiveStream по token, прерывает обе задачи и удаляет temp-файл. Вызывается: - Когда плеер переходит на следующий трек (releaseTorrentStreamUrl). - Когда SoulSeek-session заменяется через soulseek_login (все потоки сбрасываются в soulseek_logout).


HTTP-сервер для SoulSeek

HTTP-сервер в transfer.rs реализует подмножество HTTP/1.1, необходимое для конвейера Web Audio:

  • OPTIONS204 No Content с CORS-заголовками (preflight для cross-origin из WebView).
  • GET без Range → 200 OK с Content-Length: file_size, Accept-Ranges: bytes.
  • GET с Range → 206 Partial Content с корректным заголовком Content-Range.

Поведение при перемотке

Когда аудиоэлемент перематывает, он отправляет Range-запрос для нового байтового смещения. HTTP-сервер:

  1. Проверяет downloaded.load(Acquire) — количество байт, уже записанных во временный файл.
  2. Если downloaded > range_start, сразу отдаёт данные из temp-файла.
  3. Если нет — крутится в poll-цикле с интервалом 80 мс, пока downloaded > range_start или complete = true.
  4. Если данные не поступают в течение 60 секунд — возвращает 503 Service Unavailable.

Это означает, что перемотка назад мгновенна (данные уже загружены), тогда как перемотка далеко вперёд буфера может ненадолго застопориться.

HTTP-сервер читает из temp-файла в spawn_blocking через std::fs::File::seek + std::fs::File::read, чтобы не блокировать async executor. Размер чанка при чтении — HTTP_CHUNK = 32 КБ.


Расположение temp-файлов

Temp-файлы называются neegde_slsk_<token>.tmp и помещаются в std::env::temp_dir(). На macOS это /var/folders/…/T/. Они создаются в начале download_loop и удаляются командой soulseek_release_stream. Если приложение завершилось аварийно до release, файлы остаются на диске, но как правило очищаются ОС при следующей перезагрузке (temp-директория на APFS с авто-очисткой на macOS).


Переключение на альтернативных peers

Фронтенд хранит до 5 альтернативных peers в raw.alternativePeers для каждой сущности трека SoulSeek (см. Ранжирование peers). Если soulseek_prepare_stream выбрасывает ошибку (timeout, отказ, NAT), плеер автоматически повторяет попытку со следующим альтернативным peer:

// В Player.vue (упрощённо)
for (const alt of track.alternativePeers ?? []) {
  try {
    url = await streamUrl(magnet, 0, {
      source: "soulseek",
      slskUsername: alt.slskUsername,
      slskFilepath: alt.slskFilepath,
      slskFilesize: alt.size,
    });
    break;
  } catch { /* пробуем следующего */ }
}

Этот failover прозрачен для пользователя — аудио начинается, как только любой peer принимает запрос.