Files
pipu-music/docs/playback.md
T
mrleo1nidandClaude Opus 5.5 9eb1c25147 docs: add product idea, design prototypes and development documentation
- IDEA.md: product vision from the user's perspective (input for design)
- Architecture, ADRs, data model, REST API overview
- Auth and roles, library storage (NFS, path regexes, scanner, search),
  playback, recommendation system ("My Wave"), track upload queue
- Web, Android (offline, self-update), i18n, design system
- Testing strategy, Gitea Actions CI/CD, deployment, conventions, roadmap
- Clickable web/Android design prototypes and wave button variants
- .claude/launch.json to serve the prototypes locally

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:21:31 +03:00

7.2 KiB
Raw Blame History

Воспроизведение: стриминг, транскодинг, состояние плеера, офлайн

Качество

Ключ Формат Назначение
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 с до конца текущего) — на сервере это прогревает транскод.
  • Фоновая задача pretranscode для треков из выданных пачек волны (они почти наверняка будут сыграны).
  • Кэш транскодов — 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).

При старте клиента:

  1. Берётся локальная копия (localStorage / MMKV) — плеер появляется мгновенно, без ожидания сети.
  2. Параллельно GET /me/playback-state; если серверная запись новее (updated_at) и с другого устройства — применяется серверная (плеер в состоянии паузы на сохранённой позиции; автозапуск не делаем).
  3. Если трек из сохранённой очереди стал недоступен — пропускается.

Конфликты: 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.