Allinea i doc al flusso live senza OverlayRelay.

Documenta overlay in app, YoutubeRelay copy+AAC in Sidekiq
e HLS sul path camera; aggiorna checklist e troubleshooting.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-29 23:05:50 +02:00
co-authored by Cursor
parent 74a156cedc
commit a448ad59ee
6 changed files with 100 additions and 102 deletions
+49 -34
View File
@@ -1,23 +1,25 @@
# Diretta live — architettura MediaMTX
## Idea (non è folle)
**Ultimo aggiornamento:** 2026-07-29
## Idea
MediaMTX espone **un unico flusso di uscita** per path (`live/match_{uuid}`):
1. **Telefono in onda** → publisher RTMP (camera).
1. **Telefono in onda** → publisher RTMP (camera + **overlay grafico bruciato in app**).
2. **Pausa / rete assente**`alwaysAvailable` con file slate (`/slates/offline.mp4`) **senza interrompere** lHLS verso il sito.
3. **YouTube** `ffmpeg` in container Rails/Sidekiq legge **sempre** quelluscita (`-c copy`) e inoltra a `rtmp://a.rtmp.youtube.com/live2/...` per tutta la sessione.
3. **YouTube** (solo se `platform=youtube`) → `Streams::YoutubeRelay` in **Sidekiq**: un processo `ffmpeg` legge RTMP/HLS da MediaMTX e inoltra a YouTube (`-c:v copy`, audio ricodificato in AAC).
Il telefono **non** invia la copertina: risparmia banda; lo switch è lato server.
Il telefono **non** invia la copertina di pausa: risparmia banda; lo switch slate ↔ camera è lato MediaMTX.
## 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) |
| App Android / iOS | RTMP 720p AAC mono; overlay tabellone/watermark in GPU sul flusso |
| MediaMTX | Path dinamico, `alwaysAvailable` + slate, HLS |
| `Streams::YoutubeRelay` | Relay continuo verso YouTube (solo Sidekiq con `YOUTUBE_RELAY_WORKER=1`) |
| `Mediamtx::PublisherSync` | Stato Rails da API paths (publisher online) + recording on/off |
| `/hls/...` (edge → MediaMTX in prod; Rails proxy in dev) | Player web |
## Pausa e continuità
@@ -27,36 +29,44 @@ Il telefono **non** invia la copertina: risparmia banda; lo switch è lato serve
3. MediaMTX → passa alla slate sul **medesimo path** (senza interrompere luscita 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.
6. **YouTube** se attivo, il relay ffmpeg continua sulla stessa uscita MediaMTX (vede slate in pausa).
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`).
**Recording**: sul path live parte `record: false`; viene acceso via API solo con publisher online e entitlement (`PublisherSync`). I replay finiscono in Garage 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
Telefono RTMP (camera + overlay app)
MediaMTX path live/match_{uuid}
publisher ON → camera H.264/AAC (già con tabellone se overlay ≠ none)
publisher OFF → alwaysAvailable → /slates/offline.mp4
├──► HLS (segmenti ~1s) ──► edge /hls/ → MediaMTX ──► player web (HLS.js)
└──► (solo platform=youtube)
Streams::YoutubeRelay
ffmpeg: -c:v copy, -c:a aac ──► RTMPS 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`) |
| Tabellone / watermark sullo stream | **App nativa** (GPU → RTMP) |
| Continuità YouTube | **`Streams::YoutubeRelay`** (ffmpeg in Sidekiq) |
| Badge «In onda» / stato pausa | **Rails** (`status.json`, `PublisherSync`) + UI web |
| 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.
URL HLS pubblico: `https://www.matchlivetv.it/hls/live/match_{uuid}/index.m3u8` (`StreamSession#effective_hls_path_name` = path camera, **non** più `*_air`).
### Player web — anti-scatti (HLS.js)
Configurazione in `backend/app/views/public/live/show.html.erb`:
@@ -67,27 +77,32 @@ Configurazione in `backend/app/views/public/live/show.html.erb`:
- 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)
### Grafica nel video (overlay)
Tabellone, badge stato («In onda», «In pausa», «Copertina», …) e banner **Match Live TV** sono **bruciati nel flusso** da `Streams::OverlayRelay`:
**Non** c’è più un relay server `Streams::OverlayRelay` che ricodifica con PNG.
```
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)
```
Il tabellone è composito **sul telefono** prima dellRTMP (vedi [`TABELLONI_E_OVERLAY.md`](TABELLONI_E_OVERLAY.md)):
- `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** (lapp 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`.
1. `OverlayState` da `effectiveOverlayKind` + `ScoreState`
2. `OverlayRenderer` / canvas → bitmap
3. Filtro GPU sul encoder RTMP
La pagina live può mostrare badge HTML di stato (in onda / pausa / attesa); non sostituisce il tabellone nel video.
### Ruolo di ffmpeg oggi
| Quando | Cosa | Peso CPU |
|--------|------|----------|
| Diretta `platform=youtube` | `YoutubeRelay`: demux + **copy video** + AAC audio → RTMPS | Medio-basso **per diretta**, scala con N processi |
| Diretta solo `matchlivetv` | Nessun ffmpeg in live | Quasi zero |
| Fine diretta | `UploadFromSession`: concat segmenti (`-c copy`) | Picco breve |
| Post-process | `GenerateThumbnail`: un frame → JPEG | Trascurabile |
Non esiste più la ricodifica H.264 server-side delloverlay (era il carico principale).
### Punteggio (persistenza)
Lapp mobile persiste il punteggio con `PATCH /api/v1/sessions/:id/score` (`Scoring::SyncState`), poi refresh overlay.
Lapp mobile persiste il punteggio con `PATCH /api/v1/sessions/:id/score` (o `score_action`) → `Scoring::SyncState`, poi aggiorna loverlay locale in camera.
`GET /live/:id/status.json` resta per messaggi sotto il player e recovery HLS; `Cache-Control: no-store`.