Files
MatchLiveTv/docs/ARCHITECTURE.md
Emiliano Frascaro 827abf80cb Elimina i path MediaMTX orfani dopo ogni diretta.
Corregge il cleanup dei path live/match_{uuid}, rimuove sempre il path a fine sessione e lo esegue ogni ora via cron.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 20:42:40 +02:00

17 KiB
Raw Blame History

Architettura — Match Live TV

Documento di riferimento per l'intero sistema: rete, container Docker, flussi video, replay, monitoraggio ops e rilasci.

Ultimo aggiornamento: 2026-06-14
Produzione: eminux@192.168.1.146/opt/matchlivetv


Indice

  1. Panoramica
  2. Topologia di rete
  3. Stack Docker (produzione)
  4. Proxy edge (nginx)
  5. Diretta live
  6. Replay e storage
  7. Controllo applicativo
  8. Monitoraggio ops
  9. Rilasci e downtime
  10. Sviluppo locale vs produzione
  11. Documentazione correlata

Panoramica

Match Live TV è una piattaforma mobile-first per streaming di partite sportive giovanili.

Area Tecnologia
Backend Rails 7.2 (API REST, Action Cable, admin, pagine pubbliche)
Job asincroni Sidekiq + Redis
Database PostgreSQL 16
Ingest video MediaMTX (RTMP :1935)
Playback live HLS (MediaMTX :8888)
Replay Garage (S3-compatible) + MP4
App mobile Android nativo (Kotlin + Compose, RTMP); iOS nativo (SwiftUI + HaishinKit, RTMP) — parità UI/API
Edge LAN Nginx (edge) sulla porta host 3000
HTTPS pubblico Nginx Proxy Manager (NPM) → 192.168.1.146:3000

Principio guida: il traffico video pesante (HLS live, MP4 replay) non passa da Puma. Rails gestisce autenticazione, logica di business e redirect verso storage/streaming; nginx edge e MediaMTX servono i byte.


Topologia di rete

                         Internet
                             │
          ┌──────────────────┼──────────────────┐
          │                  │                  │
    Router :443        Router :1935      (opz. :8888)
          │                  │                  │
          ▼                  ▼                  ▼
   Nginx Proxy Manager   MediaMTX RTMP      MediaMTX HLS
   (LAN, TLS)           (host :1935)       (host :8888, opz.)
          │
          │  forward http://192.168.1.146:3000
          ▼
   ┌──────────────────────────────────────────────────────────┐
   │  Server 192.168.1.146 — Docker Compose (rete interna)   │
   │                                                          │
   │  ┌─────────┐     ┌─────────┐     ┌──────────┐           │
   │  │  edge   │────►│  rails  │────►│ postgres │           │
   │  │ nginx   │     │  Puma   │     └──────────┘           │
   │  │  :3000  │     └────┬────┘     ┌──────────┐           │
   │  └────┬────┘          │          │  redis   │           │
   │       │               │          └──────────┘           │
   │       │          ┌────▼────┐     ┌──────────┐           │
   │       │          │ sidekiq │────►│  garage  │           │
   │       │          └────┬────┘     │  (S3)    │           │
   │       │               │          └──────────┘           │
   │       ├───────────────┼────────► mediamtx :8888/:1935   │
   │       └───────────────┴────────► garage :3900           │
   │                                                          │
   │  ntfy :8090 (LAN) — notifiche push ops                  │
   └──────────────────────────────────────────────────────────┘
          │
          ▼
   Telefono Android ──RTMP──► :1935 (WAN)

Porte

Porta Esposizione Servizio Note
443 NPM (altro host LAN) HTTPS sito Forward a :3000 (edge)
1935 WAN → server RTMP ingest Obbligatoria per streaming da 4G
3000 Solo LAN edge nginx Punto di ingresso NPM
8090 Solo LAN ntfy Proxy HTTPS opzionale (ntfy.matchlivetv.it)
8888 Opzionale WAN HLS MediaMTX Di solito non serve: HLS passa da NPM → edge → /hls/
5432, 6379, 3900, 9997 Solo Docker DB, Redis, Garage API, MediaMTX API Mai su WAN

Stack Docker (produzione)

File: infra/docker-compose.prod.yml

Servizio Immagine / build Ruolo
postgres postgres:16-alpine Dati applicativi
redis redis:7-alpine Sidekiq, cache, campioni latenza ops
mediamtx bluenviron/mediamtx RTMP ingest, HLS, API path dinamici
garage dxflrs/garage Object storage S3 per replay MP4/thumbnail
rails build backend/ API, web, Action Cable, Puma
sidekiq build backend/ Job, relay ffmpeg (YouTube, overlay), ops
edge nginx:alpine Reverse proxy LAN, routing video, pagina cortesia
ntfy binwiederhier/ntfy Push ops su telefono

Volumi persistenti

Root dati: ${MATCHLIVETV_VIDEOS_ROOT} (produzione: /media/videos/matchlivetv)

Path host Contenuto
.../recordings Segmenti registrazione MediaMTX, staging replay
.../active_storage Allegati Rails (loghi società, ecc.)
.../log Log applicativi + cron (cron-replay.log, cron-ops.log)
.../garage/meta, .../garage/data Metadati e blob Garage
postgres_data, redis_data, ntfy_data Volumi Docker nominati

Puma

RAILS_MAX_THREADS=5 in produzione. I thread Puma sono riservati ad API, pagine HTML, WebSocket e auth — non al proxy di flussi video lunghi.


Proxy edge (nginx)

File: infra/nginx-edge/nginx.conf

NPM termina TLS e inoltra tutto il traffico HTTP a edge:80 (host :3000). Edge smista per path:

Location Destinazione Passa da Puma?
/hls/ mediamtx:8888 No
/media/ garage:3900 No
/up, /up/deep rails:3000 Sì (pass-through, no cortesia)
/cable rails:3000 Sì (WebSocket)
/ rails:3000 Sì (con pagina cortesia su 502/503/504)

HLS live (/hls/)

Introdotto per evitare saturazione Puma durante dirette con molti spettatori (migliaia di richieste segmenti .ts / .m3u8).

Browser ──► NPM ──► edge /hls/live/match_{uuid}/index.m3u8
                      │ rewrite → /live/match_{uuid}/index.m3u8
                      └──► mediamtx:8888
  • Cookie MediaMTX: se il browser non invia cookie, edge aggiunge cookieCheck=1 (map nginx).
  • proxy_buffering off, proxy_read_timeout 3600s per streaming lungo.
  • URL pubblico: https://www.matchlivetv.it/hls/live/match_{uuid}/index.m3u8 (HLS_PUBLIC_URL).

NPM: non serve più una custom location /hls verso :8888. Un unico Proxy Host verso :3000 è sufficiente; edge instrada internamente.

Fallback dev: HlsProxyController (/hls/* su Rails) resta per sviluppo locale senza container edge.

Replay media (/media/)

Introdotto per evitare che Puma faccia da proxy dei file MP4 (decine/hundreds di MB per richiesta).

Browser ──► /replay/:id/stream
                │
                ▼
            Rails: auth + Recordings::PublicMediaUrl
                │
                ▼ 302 redirect
            /media/replays/...?X-Amz-Signature=...
                │
                ▼
            edge ──► garage:3900 (Host: garage:3900, Range support)
  • REPLAY_MEDIA_REDIRECT=true (default produzione).
  • Rails genera URL presigned S3 sull'endpoint interno e lo riscrive su APP_PUBLIC_URL/media/....
  • Fallback senza Garage: Recordings::ServeFromStorage (proxy Rails, solo dev/legacy).

Pagina di cortesia

Durante restart Rails, edge risponde con HTML statico (@maintenance) invece di lasciare NPM con 502. Eccezioni: /up, /up/deep (monitoraggio ops).


Diretta live

Vedi anche LIVE_STREAMING.md per pause, overlay e player HLS.js.

Flusso ingest

App Android (RTMP 720p, AAC mono)
        │
        ▼
MediaMTX :1935 — path live/match_{uuid}  (grezzo, telefono)
        │
        ▼ overlay ffmpeg (Streams::OverlayRelay)
path live/match_{uuid}_air  (tabellone + badge bruciati nel video)
        │
        ├──► HLS :8888 ──► edge /hls/ ──► spettatori web
        └──► ffmpeg -c copy ──► YouTube RTMP (Streams::YoutubeRelay, Premium)
Componente Ruolo
Mediamtx::PathManager Crea path dinamici via API :9997
alwaysAvailable + slate Copertina /slates/offline.mp4 senza interrompere HLS
Streams::OverlayRelay Ricodifica con PNG 1280×720 aggiornata ogni ~2s
Streams::YoutubeRelay Legge HLS da mediamtx:8888 (non più via Rails)
Mediamtx::PublisherSync Stato publisher online/offline in Rails

Pausa e continuità

  1. App chiama PATCH /sessions/:id/pause → stop RTMP.
  2. MediaMTX passa alla slate sullo stesso path (stesso URL HLS).
  3. YouTube relay continua sulla stessa uscita (vede slate).
  4. Ripresa: resume → app ripubblica RTMP; MediaMTX concatena camera senza nuovo URL.

Recording inline MediaMTX disabilitato (record: false su path live): il recorder al switch slate→camera distruggeva il muxer HLS. I replay usano job dedicato (vedi sotto).

Pagina live web

  • URL: /live/:session_id
  • Player: HLS.js su HLS_PUBLIC_URL/live/match_{uuid}_air/index.m3u8
  • Stato: GET /live/:id/status.json (badge, recovery buffer, no-store)

Replay e storage

Vedi REPLAY_MODULE.md per piani, retention, cron e API.

Pipeline post-diretta

Sessions::Stop → FinalizeSession → UploadJob
        │
        ▼
Garage bucket matchlivetv-replays (MP4 + thumbnail)
        │
        ├──► Email «replay pronto»
        ├──► /replay/:id (player web)
        └──► PublishToYoutubeJob (Premium Full, se diretta era su YouTube)

Delivery al browser

Endpoint Rails Azione
GET /replay/:id Pagina player
GET /replay/:id/stream Auth → redirect 302 → /media/...
GET /replay/:id/thumbnail Auth → redirect 302 → /media/...
GET /replay/:id/download Auth premium → redirect attachment

YouTube — doppia presenza (Premium Full)

Tab / canale Contenuto
Live (diretta YouTube) Archivio stream live sul canale
Video (VOD) Republicazione MP4 automatica via PublishToYoutubeJob

Controllo applicativo

App Android ◄──── REST API (/api/v1/...) ────► Rails
     │                                              │
     │         Action Cable (/cable)                │
     └──────────────────────────────────────────────┘
                              │
                              ▼
                         PostgreSQL
                              │
     Sidekiq ◄── Redis ◄──────┘
        │
        ├── HealthMonitorJob, UploadJob, overlay refresh
        ├── YoutubeRelay, OverlayRelay (ffmpeg)
        └── PublishToYoutubeJob, purge replay, ecc.
Canale Uso
REST JWT Login, sessioni, punteggio, configurazione stream
Action Cable Tabellone live, aggiornamenti regia (in sviluppo su app)
Webhook MediaMTX Eventi publisher connect/disconnect

Monitoraggio ops

Vedi OPS_MONITORING.md per setup ntfy e troubleshooting.

Componenti

Pezzo Ruolo
Ops::HealthMonitorJob Sidekiq, ogni ~3 min
Ops::HealthChecks Esegue tutti i check, apre/chiude incidenti
Ops::UpLatencyTracker Campioni latenza in Redis, calcolo p95
Ops::LogScanner Cron ogni 10 min su log Docker
Ops::Incident + /admin/ops Dashboard e storico
GET /up/deep JSON esteso (token X-Ops-Token)

Check HTTP (evoluzione 2026-06)

Kind Severità Cosa verifica Frequenza
http_rails critical GET http://edge/up (rete Docker, no hairpin NPM) Ogni ciclo (~3 min)
rails_latency warning / critical p95 latenza campioni http_rails Ogni ciclo (dopo ≥5 campioni)
http_public warning GET https://www.matchlivetv.it/up Ogni 15 min (OPS_HTTP_PUBLIC_INTERVAL_SECS)

Perché la separazione: il check dall'interno del container Rails verso l'URL pubblico può fallire per hairpin NAT / timeout NPM anche quando il sito è raggiungibile dall'esterno. http_rails misura la salute reale dello stack; http_public è un controllo complementare non bloccante.

Soglie latenza p95

Variabile Default
OPS_UP_LATENCY_WARN_MS 2000
OPS_UP_LATENCY_CRIT_MS 5000
OPS_UP_LATENCY_SAMPLES 60
OPS_UP_LATENCY_MIN_SAMPLES 5

http_rails registra ogni misura in Redis; check_rails_latency calcola p95 e apre incidente rails_latency se supera soglia.

Altri check

Disco root, dimensione archivio registrazioni, PostgreSQL, Redis, MediaMTX API, Garage head-bucket, heartbeat Sidekiq, dead queue Sidekiq.

Notifiche

Default OPS_NOTIFY_SEVERITIES=criticalhttp_public (warning) e rails_latency (warning) non notificano; critical sì (ntfy + email opzionale).


Rilasci e downtime

Script: scripts/deploy/release_production.sh

Dev machine                          Server produzione
     │                                      │
     │  rsync (sync_to_server.sh)           │
     ├─────────────────────────────────────►│
     │                                      │
     │                              build rails + sidekiq
     │                              migrate DB
     │                              force-recreate edge
     │                              recreate rails (+ wait /up)
     │                              recreate sidekiq
     │                              install cron
Fase Impatto utente
edge recreate Nessuno (nginx torna subito)
rails recreate Pagina cortesia ~3090s; /up brevemente down
RTMP / diretta attiva Non interrotta (MediaMTX indipendente)
Replay in riproduzione Continuano via /media/ → Garage (edge resta su)

Smoke test post-rilascio:

curl -sf https://www.matchlivetv.it/up
curl -sI https://www.matchlivetv.it/hls/   # 404 MediaMTX = routing OK
docker compose exec rails bundle exec rails runner 'puts Ops::HealthChecks.new.summary[:status]'

Sviluppo locale vs produzione

Aspetto Dev (docker compose) Produzione
Edge nginx Assente (NPM simulato con porta 3000 → rails) Container edge obbligatorio
HLS playback HlsProxyController su Rails edge → MediaMTX
Replay stream Redirect o proxy Rails Redirect → edge → Garage
Codice Rails Volume mount hot-reload Immagine Docker buildata
MediaMTX :1935/:8888 Esposti su localhost :1935 WAN, :8888 opzionale

Avvio locale:

cd infra && cp .env.example .env && docker compose up -d --build

Documentazione correlata

Documento Contenuto
infrastructure/SERVER_DEPLOYMENT.md Bootstrap server, NPM, cron, backup
LIVE_STREAMING.md MediaMTX, pause, overlay, HLS.js
REPLAY_MODULE.md Garage, retention, YouTube VOD
OPS_MONITORING.md ntfy, variabili ops, troubleshooting
TABELLONI_E_OVERLAY.md Grafica in stream
PRODUCT_PREMIUM.md Piani e funzionalità premium
native/android/README.md App Android

Cronologia modifiche architetturali rilevanti

Data Modifica
2026-05 Introduzione container edge + pagina cortesia ai rilasci
2026-06 Replay: redirect 302 a /media/ → Garage (fuori da Puma)
2026-06 HLS live: edge → MediaMTX (fuori da Puma)
2026-06 Ops: check http_rails + http_public separati, latenza p95 UpLatencyTracker
2026-06 RAILS_MAX_THREADS=5; relay YouTube legge mediamtx:8888 diretto
2026-06 Cleanup path MediaMTX orfani (Mediamtx::CleanupOrphanPaths); delete path sempre a fine diretta