Quality upgrades, batch covers, staging preview, and resume after reload are in the queue. Loudness and crossfade work on web and Android, and the documented checks now have suites. Co-authored-by: Cursor <cursoragent@cursor.com>
7.3 KiB
Воспроизведение: стриминг, транскодинг, состояние плеера, офлайн
Качество
| Ключ | Формат | Назначение |
|---|---|---|
original |
исходный файл | Веб по умолчанию в локальной сети, «без потерь» |
high |
Opus 256 kbps (Ogg) | Мобильное по Wi-Fi, загрузки по умолчанию |
medium |
Opus 128 kbps | Мобильное по сотовой сети |
low |
Opus 64 kbps | Экономия трафика |
auto |
клиент выбирает по типу сети / настройкам |
Если исходник — lossy с битрейтом ниже целевого (например, MP3 192 при запросе high), отдаётся оригинал
(перекодирование вверх бессмысленно). Браузерные исключения: исходный формат, который браузер не играет
(APE, WV, AIFF в некоторых браузерах), автоматически транскодируется в high.
Отдача файла
GET /api/v1/stream/{trackId}?q=high&exp=1760900000&sig=…
→ api: проверка sig/exp, пользователь не заблокирован, трек доступен
→ если нужен транскод и его нет в кэше:
ffmpeg -i <src> -map 0:a -c:a libopus -b:a 256k -vbr on -f ogg <cache>/<trackId>/high.opus.tmp
(атомарный rename; single-flight через Redis-лок, чтобы два запроса не транскодировали одно и то же)
→ 200 с заголовками:
X-Accel-Redirect: /_internal/library/<source_id>/<rel_path> (или /_internal/cache/<trackId>/high.opus)
Content-Type: audio/flac | audio/ogg; codecs=opus | ...
Cache-Control: private, max-age=43200
nginx:
location /_internal/library/ { internal; alias /library/; sendfile on; aio threads; }
location /_internal/cache/ { internal; alias /cache/transcode/; sendfile on; }
- Транскод трека 4 мин в Opus занимает ~1–2 с на одном ядре, поэтому делаем синхронно при первом запросе.
Клиенты предзагружают следующий трек очереди (запрос
HEAD/Range: bytes=0-0за 20 с до конца текущего) — на сервере это прогревает транскод. - Фоновая подготовка: выдача пачки волны вызывает
playback.Prewarm— для качестваhighтранскод строится сразу, не дожидаясь первогоRangeклиента. - Кэш транскодов — LRU с лимитом
TRANSCODE_CACHE_LIMIT_GB(по умолчанию 50 ГБ), чистка поatimeраз в час. - Пул ffmpeg ограничен (
TRANSCODE_CONCURRENCY, по умолчанию = число ядер / 2).
Нормализация громкости
track_features.loudness_lufs отдаётся в DTO; клиенты применяют ReplayGain-подобное усиление к целевым −14 LUFS
(веб — GainNode, Android — react-native-track-player volume на трек). Включается в настройках, по умолчанию вкл.
Состояние плеера («запомнить трек и момент»)
Сервер хранит одну запись playback_state на пользователя.
Клиент отправляет PUT /me/playback-state:
- каждые 10 с во время воспроизведения;
- на паузе, переключении трека, перемотке (с debounce 1 с);
- при закрытии вкладки / сворачивании приложения (
navigator.sendBeaconна вебе,AppStateна Android).
При старте клиента:
- Берётся локальная копия (localStorage / MMKV) — плеер появляется мгновенно, без ожидания сети.
- Параллельно
GET /me/playback-state; если серверная запись новее (updated_at) и с другого устройства — применяется серверная (плеер в состоянии паузы на сохранённой позиции; автозапуск не делаем). - Если трек из сохранённой очереди стал недоступен — пропускается.
Конфликты: last-write-wins по updated_at. Для волны в context хранится wave_session_id: при восстановлении
волна продолжает ту же сессию (если ей < 12 ч) или начинает новую с теми же занятием / настроением.
События прослушивания
Клиентский модуль packages/core/playback-events формирует события и буферизует их:
| Событие | Когда | Ключевые поля |
|---|---|---|
play_start |
Начало воспроизведения трека | context_type, context_id, wave_session_id |
play_end |
Трек доигран до конца (или осталось < 5 с) | played_ms |
skip |
Переключение до конца | position_ms, played_ms |
seek |
Перемотка | from_ms, position_ms |
pause / resume |
— (только для аналитики сессии, в обучении не используются) | |
like / unlike / dislike / undislike |
Реакция | position_ms |
played_msсчитается как реально прослушанное время (перемотки вперёд не засчитываются).- Отправка: пачкой раз в 30 с, при смене трека и при уходе в фон; при ошибке — повтор с экспоненциальной задержкой.
- Каждое событие имеет
id(UUIDv7) — сервер дедуплицирует. - Засчитанное прослушивание (для
play_count, истории и популярности):played_ms ≥ min(50% длительности, 4 мин)либоplay_end. В историю попадают всеplay_start, у которыхplayed_ms ≥ 30 сили трек короче 30 с.
Офлайн (Android)
Подробности клиента — mobile.md. Серверная сторона:
GET /tracks/{id}/download-url?q=high→ подписанная ссылка на 1 ч (тот же/stream, но сdl=1:Content-Disposition: attachment, засчитывается не как прослушивание).- Офлайн-события приходят позже с
offline=trueи реальнымoccurred_at; сервер принимает события давностью до 30 дней. - Лимит: не более 2 параллельных загрузок на пользователя (rate limit в Redis), чтобы не забить канал NFS.