Files
MatchLiveTv/docs/ARCHITECTURE.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

416 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) |
| 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 ~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:
```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 |