Aggiunge docs/ARCHITECTURE.md con topologia rete, edge nginx, flussi HLS/replay e monitoraggio ops; allinea i documenti esistenti. Co-authored-by: Cursor <cursoragent@cursor.com>
103 lines
5.7 KiB
Markdown
103 lines
5.7 KiB
Markdown
# Diretta live — architettura MediaMTX
|
||
|
||
## Idea (non è folle)
|
||
|
||
MediaMTX espone **un unico flusso di uscita** per path (`live/match_{uuid}`):
|
||
|
||
1. **Telefono in onda** → publisher RTMP (camera).
|
||
2. **Pausa / rete assente** → `alwaysAvailable` con file slate (`/slates/offline.mp4`) **senza interrompere** l’HLS verso il sito.
|
||
3. **YouTube** → `ffmpeg` in container Rails/Sidekiq legge **sempre** quell’uscita (`-c copy`) e inoltra a `rtmp://a.rtmp.youtube.com/live2/...` per tutta la sessione.
|
||
|
||
Il telefono **non** invia la copertina: risparmia banda; lo switch è lato server.
|
||
|
||
## Componenti
|
||
|
||
| Pezzo | Ruolo |
|
||
|--------|--------|
|
||
| App Android | RTMP 48 kHz mono, 720p — solo quando in onda |
|
||
| MediaMTX | Path dinamico, `alwaysAvailable` + slate |
|
||
| `Streams::YoutubeRelay` | Relay continuo verso YouTube (no hook wget su distroless) |
|
||
| `Mediamtx::PublisherSync` | Stato Rails da API paths (publisher online) |
|
||
| `/hls/...` (edge → MediaMTX in prod; Rails proxy in dev) | Player web |
|
||
|
||
## Pausa e continuità
|
||
|
||
1. API `PATCH /sessions/:id/pause` → stato `paused`, registrazione off.
|
||
2. App → `pauseStream()` (stop RTMP).
|
||
3. MediaMTX → passa alla slate sul **medesimo path** (senza interrompere l’uscita HLS).
|
||
4. **Ripresa** → API `resume` → `connecting`, recording on, app `resumeStream()`; MediaMTX concatena di nuovo camera sulla stessa uscita HLS.
|
||
5. **Sito** → **un solo** player HLS per tutta la sessione (mai reload in pausa/ripresa). Copertina brand (`brand/CopertinaCanale_6.png` → `infra/slates/offline.mp4`) muxata da MediaMTX; in pausa solo un messaggio leggero sopra il video.
|
||
6. **YouTube** → relay ffmpeg continuo; vede la stessa uscita MediaMTX.
|
||
|
||
Non fare `patch` del path MediaMTX in pausa: ricarica il path e interrompe gli HLS reader.
|
||
|
||
**Recording**: disabilitato sul path live (`record: false`) — il recorder MediaMTX al switch slate→camera distruggeva il muxer HLS (`too many reordered frames`). I replay vanno gestiti con job dedicato (vedi `REPLAY_MODULE.md`).
|
||
|
||
Il player web resta attivo finché `ready|available|online` sul path (non solo quando il telefono è `online`).
|
||
|
||
## Infrastruttura vs browser
|
||
|
||
```
|
||
Telefono RTMP ──► MediaMTX path live/match_{uuid}
|
||
│
|
||
publisher ON ├─► camera (H.264/AAC)
|
||
publisher OFF└─► alwaysAvailable → /slates/offline.mp4
|
||
│
|
||
├─► HLS (segmenti ~1s) ──► edge /hls/ → MediaMTX (prod) o proxy Rails (dev) ──► player web (HLS.js)
|
||
└─► RTMP lettura ──► Streams::YoutubeRelay (ffmpeg -c copy) ──► YouTube
|
||
```
|
||
|
||
| Responsabilità | Dove |
|
||
|----------------|------|
|
||
| Switch copertina ↔ live | **MediaMTX** (stesso path, stesso URL HLS) |
|
||
| Continuità YouTube | **Relay ffmpeg** sulla stessa uscita |
|
||
| Badge «In onda» / stato pausa | **Rails** (`status.json`, `PublisherSync`) |
|
||
| Uscire dal buffer copertina dopo ripresa | **Browser** (`pendingLiveRecovery`, reload HLS) |
|
||
| Fluidità playback | **Browser** (evitare seek continui su `liveSyncPosition`) |
|
||
|
||
Lo switch slate→camera **non** richiede un nuovo URL lato player: MediaMTX concatena sulla playlist. Il sito può però restare «indietro» nel buffer HLS (ancora segmenti della slate) anche con `publisher_online: true` — da qui i recovery controllati in `show.html.erb`, **senza** chiamare `jumpToLiveEdge()` a ogni frammento bufferizzato.
|
||
|
||
### Player web — anti-scatti (HLS.js)
|
||
|
||
Configurazione in `backend/app/views/public/live/show.html.erb`:
|
||
|
||
- `maxLiveSyncPlaybackRate: 1.08` (evita accelerazioni visibili; prima 1.5 causava micro-scatti).
|
||
- `jumpToLiveEdge(force)` con throttle: seek solo se lag > 2s o `force`, e non più di una volta ogni ~8s se lag < 4s.
|
||
- **Niente** seek su `FRAG_BUFFERED` né nel poll ogni 1.5s.
|
||
- Sync iniziale una volta su `LEVEL_LOADED`; recovery solo se `liveEdgeLagSec() > 4` o `pendingLiveRecovery`.
|
||
- Interval 6s solo durante recovery attivo (`tryEscapeCoverBuffer`).
|
||
|
||
### Grafica nel video (overlay server)
|
||
|
||
Tabellone, badge stato («In onda», «In pausa», «Copertina», …) e banner **Match Live TV** sono **bruciati nel flusso** da `Streams::OverlayRelay`:
|
||
|
||
```
|
||
Telefono → live/match_{uuid} (grezzo)
|
||
└─► ffmpeg + PNG 1280×720 (Streams::Overlay::Refresh ogni ~2s)
|
||
└─► live/match_{uuid}_air
|
||
├─► HLS (sito, anche fullscreen)
|
||
└─► YouTube relay (-c copy)
|
||
```
|
||
|
||
- `Streams::Overlay::SvgBuilder` + `rsvg-convert` generano `tmp/stream_overlays/{session_id}/overlay.png`
|
||
- `Streams::Overlay::Refresh` + `bin/overlay_png_feeder.rb` aggiornano la PNG ogni ~2s (e su punteggio/stato); il feeder la invia a ffmpeg via FIFO perché `-loop 1` non rilegge il file su disco
|
||
- Volume Docker condiviso `stream_overlays` tra **rails** (ffmpeg) e **sidekiq** (OverlayRefreshJob); altrimenti il job scrive PNG su un filesystem diverso e badge/punteggio restano congelati
|
||
- La pagina live **non** sovrappone più HTML sul player (evita incongruenze con fullscreen / YouTube)
|
||
- **Qualità video**: il relay normalizza a **30 fps CFR** (l’app RTMP può inviare timestamp errati) e ricodifica a **≥4.5 Mbps** (`fast`, no B-frame). Override: `OVERLAY_RELAY_VIDEO_KBPS`, `OVERLAY_RELAY_X264_PRESET`, `OVERLAY_RELAY_FPS`.
|
||
|
||
### Punteggio (persistenza)
|
||
|
||
L’app mobile persiste il punteggio con `PATCH /api/v1/sessions/:id/score` (`Scoring::SyncState`), poi refresh overlay.
|
||
|
||
`GET /live/:id/status.json` resta per messaggi sotto il player e recovery HLS; `Cache-Control: no-store`.
|
||
|
||
## Rigenerare la slate
|
||
|
||
```bash
|
||
cd infra
|
||
bash scripts/generate_slate.sh
|
||
# Copia su server: infra/slates/offline.mp4 montato in /slates nel container mediamtx
|
||
```
|
||
|
||
Formato obbligatorio: **H.264 1280×720 30fps, AAC 48 kHz mono** (allineato all’app).
|