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>
This commit is contained in:
1 parent
36e99fe960
commit
9eb1c25147
30 files changed
+7538
No files matched your search
@@ -0,0 +1,92 @@
|
||||
# Воспроизведение: стриминг, транскодинг, состояние плеера, офлайн
|
||||
|
||||
## Качество
|
||||
|
||||
| Ключ | Формат | Назначение |
|
||||
|---|---|---|
|
||||
| `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:
|
||||
```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](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.
|
||||
Reference in new issue
Block a user