Files
MatchLiveTv/docs/LIVE_STREAMING.md
Emiliano Frascaro 498f6069d0 Documenta l'architettura completa del sistema.
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>
2026-06-14 21:26:35 +02:00

5.7 KiB
Raw Blame History

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 assentealwaysAvailable con file slate (/slates/offline.mp4) senza interrompere lHLS verso il sito.
  3. YouTubeffmpeg in container Rails/Sidekiq legge sempre quelluscita (-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 luscita HLS).
  4. Ripresa → API resumeconnecting, recording on, app resumeStream(); MediaMTX concatena di nuovo camera sulla stessa uscita HLS.
  5. Sitoun solo player HLS per tutta la sessione (mai reload in pausa/ripresa). Copertina brand (brand/CopertinaCanale_6.pnginfra/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 (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.

Punteggio (persistenza)

Lapp 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 allapp).