Porting SwiftUI da Android (auth, hub, wizard, broadcast RTMP, overlay, scoring board-aware), reload hub al ritorno da diretta, decodifica score tollerante e documentazione allineata. Co-authored-by: Cursor <cursoragent@cursor.com>
17 KiB
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
- Panoramica
- Topologia di rete
- Stack Docker (produzione)
- Proxy edge (nginx)
- Diretta live
- Replay e storage
- Controllo applicativo
- Monitoraggio ops
- Rilasci e downtime
- Sviluppo locale vs produzione
- 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 3600sper 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à
- App chiama
PATCH /sessions/:id/pause→ stop RTMP. - MediaMTX passa alla slate sullo stesso path (stesso URL HLS).
- YouTube relay continua sulla stessa uscita (vede slate).
- 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=critical → http_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 ~30–90s; /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 |