Tabellone, badge e brand sono ricodificati nel flusso _air via OverlayRelay; sync punteggio HTTP, volume condiviso rails/sidekiq, qualità 720p migliorata e badge CONNECTING in ripresa da pausa. Co-authored-by: Cursor <cursoragent@cursor.com>
5.7 KiB
Diretta live — architettura MediaMTX
Idea (non è folle)
MediaMTX espone un unico flusso di uscita per path (live/match_{uuid}):
- Telefono in onda → publisher RTMP (camera).
- Pausa / rete assente →
alwaysAvailablecon file slate (/slates/offline.mp4) senza interrompere l’HLS verso il sito. - YouTube →
ffmpegin container Rails/Sidekiq legge sempre quell’uscita (-c copy) e inoltra artmp://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/... (Rails proxy) |
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 → 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) ──► proxy Rails /hls/... ──► 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 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 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-convertgeneranotmp/stream_overlays/{session_id}/overlay.pngStreams::Overlay::Refresh+bin/overlay_png_feeder.rbaggiornano la PNG ogni ~2s (e su punteggio/stato); il feeder la invia a ffmpeg via FIFO perché-loop 1non rilegge il file su disco- Volume Docker condiviso
stream_overlaystra 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
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).