# Diretta live — architettura MediaMTX **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 + **overlay grafico bruciato in app**). 2. **Pausa / rete assente** → `alwaysAvailable` con file slate (`/slates/offline.mp4`) **senza interrompere** l’HLS verso il sito. 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`, `-c:a copy`; l’AAC 48 kHz mono è già prodotto dall’app / slate). Il telefono **non** invia la copertina di pausa: risparmia banda; lo switch slate ↔ camera è lato MediaMTX. ## Componenti | Pezzo | Ruolo | |--------|--------| | 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à 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** → 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**: 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 (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 copy ──► RTMPS YouTube ``` | Responsabilità | Dove | |----------------|------| | Switch copertina ↔ live | **MediaMTX** (stesso path, stesso URL HLS) | | 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`: - `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) **Non** c’è più un relay server `Streams::OverlayRelay` che ricodifica con PNG. Il tabellone è composito **sul telefono** prima dell’RTMP (vedi [`TABELLONI_E_OVERLAY.md`](TABELLONI_E_OVERLAY.md)): 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/audio** → RTMPS (remux) | 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 dell’overlay (era il carico principale). ### Punteggio (persistenza) L’app mobile persiste il punteggio con `PATCH /api/v1/sessions/:id/score` (o `score_action`) → `Scoring::SyncState`, poi aggiorna l’overlay locale in camera. `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).