6.8 KiB
Diretta live — architettura MediaMTX
Ultimo aggiornamento: 2026-08-12
Idea
MediaMTX espone un unico flusso di uscita per path (live/match_{uuid}):
- Telefono in onda → publisher RTMP (camera + overlay grafico bruciato in app).
- Pausa / rete assente →
alwaysAvailablecon file slate (/slates/offline.mp4) senza interrompere l’HLS verso il sito. - YouTube (solo se
platform=youtube) →Streams::YoutubeRelayin Sidekiq: un processoffmpeglegge 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à
- API
PATCH /sessions/:id/pause→ statopaused, registrazione off. - App →
pauseStream()(stop RTMP). - MediaMTX → passa alla slate sul medesimo path (senza interrompere l’uscita HLS).
- Ripresa → API
resume→connecting, recording on, appresumeStream(); MediaMTX concatena di nuovo camera sulla stessa uscita HLS. - 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. - 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 oforce, e non più di una volta ogni ~8s se lag < 4s.- Niente seek su
FRAG_BUFFEREDné nel poll ogni 1.5s. - Sync iniziale una volta su
LEVEL_LOADED; recovery solo seliveEdgeLagSec() > 4opendingLiveRecovery. - 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):
OverlayStatedaeffectiveOverlayKind+ScoreStateOverlayRenderer/ canvas → bitmap- 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.
Mute audio (copyright palestra / YouTube)
Se in palestra c’è musica di sottofondo, YouTube può reclamare il copyright sul flusso inoltrato (-c:a copy). L’operatore può silenziare il microfono senza interrompere video, overlay, HLS o relay:
- Lo stato
audio_mutedè persistito sustream_sessions(defaultfalse). - API
PATCH /api/v1/sessions/:id/audio_mute{ "muted": true|false }ePOST /regia/:token/audio_mute. - Action Cable:
{ type: "command", action: "mute_audio"|"unmute_audio" }e{ type: "stream_event", event: "audio_muted", muted: true|false }. - L’app non toglie la traccia AAC: azzera i sample PCM (silenzio). YouTube e MediaMTX continuano a ricevere audio 48 kHz mono.
Pulsante mute: overlay diretta (Android/iOS, accanto a pausa) e pagina regia.
Dettaglio implementazione iOS: IOS_STREAM_AUDIO_MUTE.md.
Rigenerare la slate
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).