# 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](#panoramica) 2. [Topologia di rete](#topologia-di-rete) 3. [Stack Docker (produzione)](#stack-docker-produzione) 4. [Proxy edge (nginx)](#proxy-edge-nginx) 5. [Diretta live](#diretta-live) 6. [Replay e storage](#replay-e-storage) 7. [Controllo applicativo](#controllo-applicativo) 8. [Monitoraggio ops](#monitoraggio-ops) 9. [Rilasci e downtime](#rilasci-e-downtime) 10. [Sviluppo locale vs produzione](#sviluppo-locale-vs-produzione) 11. [Documentazione correlata](#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`](../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`](../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`](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`](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`](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`](../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: ```bash 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: ```bash cd infra && cp .env.example .env && docker compose up -d --build ``` --- ## Documentazione correlata | Documento | Contenuto | |-----------|-----------| | [`infrastructure/SERVER_DEPLOYMENT.md`](infrastructure/SERVER_DEPLOYMENT.md) | Bootstrap server, NPM, cron, backup | | [`LIVE_STREAMING.md`](LIVE_STREAMING.md) | MediaMTX, pause, overlay, HLS.js | | [`REPLAY_MODULE.md`](REPLAY_MODULE.md) | Garage, retention, YouTube VOD | | [`OPS_MONITORING.md`](OPS_MONITORING.md) | ntfy, variabili ops, troubleshooting | | [`TABELLONI_E_OVERLAY.md`](TABELLONI_E_OVERLAY.md) | Grafica in stream | | [`PRODUCT_PREMIUM.md`](PRODUCT_PREMIUM.md) | Piani e funzionalità premium | | [`native/android/README.md`](../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 |