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>
This commit is contained in:
@@ -58,8 +58,13 @@ Vedi [native/android/README.md](native/android/README.md).
|
|||||||
|
|
||||||
## Architettura
|
## Architettura
|
||||||
|
|
||||||
- Video (MVP): Phone → MediaMTX (RTMP) → HLS su `matchlivetv.*` (pagina `/live/:session_id`)
|
Documentazione completa: **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)** (rete, edge nginx, flussi video, ops).
|
||||||
- Video (premium): stesso ingest + relay ffmpeg verso YouTube
|
|
||||||
|
In sintesi:
|
||||||
|
|
||||||
|
- Video live: Phone → MediaMTX (RTMP) → HLS via edge `/hls/` (non Puma)
|
||||||
|
- Video replay: Rails auth → redirect `/media/` → Garage (non Puma)
|
||||||
|
- Video premium: stesso ingest + relay ffmpeg verso YouTube
|
||||||
- Controllo: Phone ↔ Action Cable ↔ Rails ↔ PostgreSQL
|
- Controllo: Phone ↔ Action Cable ↔ Rails ↔ PostgreSQL
|
||||||
- Disconnessioni: MediaMTX `alwaysAvailable` slate + timeout 5 min (Sidekiq)
|
- Disconnessioni: MediaMTX `alwaysAvailable` slate + timeout 5 min (Sidekiq)
|
||||||
|
|
||||||
|
|||||||
415
docs/ARCHITECTURE.md
Normal file
415
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
# 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 ~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 |
|
||||||
@@ -18,7 +18,7 @@ Il telefono **non** invia la copertina: risparmia banda; lo switch è lato serve
|
|||||||
| MediaMTX | Path dinamico, `alwaysAvailable` + slate |
|
| MediaMTX | Path dinamico, `alwaysAvailable` + slate |
|
||||||
| `Streams::YoutubeRelay` | Relay continuo verso YouTube (no hook wget su distroless) |
|
| `Streams::YoutubeRelay` | Relay continuo verso YouTube (no hook wget su distroless) |
|
||||||
| `Mediamtx::PublisherSync` | Stato Rails da API paths (publisher online) |
|
| `Mediamtx::PublisherSync` | Stato Rails da API paths (publisher online) |
|
||||||
| `/hls/...` (Rails proxy) | Player web |
|
| `/hls/...` (edge → MediaMTX in prod; Rails proxy in dev) | Player web |
|
||||||
|
|
||||||
## Pausa e continuità
|
## Pausa e continuità
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ Telefono RTMP ──► MediaMTX path live/match_{uuid}
|
|||||||
publisher ON ├─► camera (H.264/AAC)
|
publisher ON ├─► camera (H.264/AAC)
|
||||||
publisher OFF└─► alwaysAvailable → /slates/offline.mp4
|
publisher OFF└─► alwaysAvailable → /slates/offline.mp4
|
||||||
│
|
│
|
||||||
├─► HLS (segmenti ~1s) ──► proxy Rails /hls/... ──► player web (HLS.js)
|
├─► HLS (segmenti ~1s) ──► edge /hls/ → MediaMTX (prod) o proxy Rails (dev) ──► player web (HLS.js)
|
||||||
└─► RTMP lettura ──► Streams::YoutubeRelay (ffmpeg -c copy) ──► YouTube
|
└─► RTMP lettura ──► Streams::YoutubeRelay (ffmpeg -c copy) ──► YouTube
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ MediaMTX registra **solo mentre il telefono pubblica** (RTMP connesso). In pausa
|
|||||||
|
|
||||||
### 4. Riproduzione
|
### 4. Riproduzione
|
||||||
- Player MP4: `/replay/:id` + stream `/replay/:id/stream`
|
- Player MP4: `/replay/:id` + stream `/replay/:id/stream`
|
||||||
|
- Delivery: Rails verifica accesso → redirect 302 a `/media/...` (edge → Garage). Puma non proxya il file.
|
||||||
- Contatore visualizzazioni su ogni play (`view_count`)
|
- Contatore visualizzazioni su ogni play (`view_count`)
|
||||||
|
|
||||||
### 5. Visibilità
|
### 5. Visibilità
|
||||||
@@ -124,8 +125,8 @@ bash scripts/garage_set_capacity.sh 85G # oppure senza argomento: calcolo auto
|
|||||||
|
|
||||||
| Origine diretta | Copia Garage/S3 | Player sito `/replay/:id` | YouTube VOD |
|
| Origine diretta | Copia Garage/S3 | Player sito `/replay/:id` | YouTube VOD |
|
||||||
|-----------------|-----------------|----------------------------|-------------|
|
|-----------------|-----------------|----------------------------|-------------|
|
||||||
| `matchlivetv` | Sì (Premium) | MP4 via proxy Rails | Manuale (Premium Full) |
|
| `matchlivetv` | Sì (Premium) | MP4 via redirect `/media/` → Garage | Manuale (Premium Full) |
|
||||||
| `youtube` | Sì (Premium) | MP4 via proxy Rails | Auto se Premium Full + diretta YouTube |
|
| `youtube` | Sì (Premium) | MP4 via redirect `/media/` → Garage | Auto se Premium Full + diretta YouTube |
|
||||||
|
|
||||||
Se il file S3 non è disponibile ma esiste `youtube_video_id`, la pagina replay mostra embed YouTube.
|
Se il file S3 non è disponibile ma esiste `youtube_video_id`, la pagina replay mostra embed YouTube.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
**Server:** `eminux@192.168.1.146` (Debian 13)
|
**Server:** `eminux@192.168.1.146` (Debian 13)
|
||||||
**Path deploy:** `~/matchlivetv` (dopo bootstrap opzionale: `/opt/matchlivetv`)
|
**Path deploy:** `~/matchlivetv` (dopo bootstrap opzionale: `/opt/matchlivetv`)
|
||||||
**Ultimo aggiornamento:** 2026-05-27
|
**Ultimo aggiornamento:** 2026-06-14
|
||||||
|
|
||||||
|
> Architettura completa: [ARCHITECTURE.md](../ARCHITECTURE.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,12 +13,15 @@
|
|||||||
```
|
```
|
||||||
Internet
|
Internet
|
||||||
│
|
│
|
||||||
├──► Router WAN :443 ──► Nginx Proxy Manager (LAN) ──► http://192.168.1.146:3000 (Rails API + Action Cable)
|
├──► Router WAN :443 ──► Nginx Proxy Manager (LAN) ──► http://192.168.1.146:3000 (edge nginx)
|
||||||
|
│ ├── /hls/ → MediaMTX :8888
|
||||||
|
│ ├── /media/ → Garage :3900
|
||||||
|
│ └── / → Rails (API + web)
|
||||||
│
|
│
|
||||||
└──► Router WAN :1935 ──► MediaMTX RTMP (telefoni, ingest video diretto)
|
└──► Router WAN :1935 ──► MediaMTX RTMP (telefoni, ingest video diretto)
|
||||||
```
|
```
|
||||||
|
|
||||||
Il flusso video **non passa da Rails**: telefono → RTMP :1935 → MediaMTX → relay ffmpeg → YouTube.
|
Il flusso video live e replay **non passa da Puma**: HLS e MP4 sono serviti da edge verso MediaMTX e Garage; Rails fa auth e redirect.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -68,11 +73,7 @@ Il flusso video **non passa da Rails**: telefono → RTMP :1935 → MediaMTX →
|
|||||||
Durante i rilasci Rails viene mostrata una **pagina di cortesia** invece del 502 bianco di NPM.
|
Durante i rilasci Rails viene mostrata una **pagina di cortesia** invece del 502 bianco di NPM.
|
||||||
|
|
||||||
2. **Websockets:** ON (obbligatorio per `/cable`)
|
2. **Websockets:** ON (obbligatorio per `/cable`)
|
||||||
3. **Custom location HLS** (spettatori):
|
3. **HLS:** nessuna custom location necessaria — edge instrada `/hls/` verso MediaMTX internamente. Un solo Proxy Host verso `:3000` basta.
|
||||||
- Location: `/hls`
|
|
||||||
- Forward: `http://192.168.1.146:8888` (in NPM usare proxy pass che **rimuove** il prefisso `/hls`, così MediaMTX riceve `/live/match_{uuid}/index.m3u8`)
|
|
||||||
- Scheme: http
|
|
||||||
- URL pubblico: `https://www.matchlivetv.it/hls/live/match_{uuid}/index.m3u8`
|
|
||||||
4. SSL Let's Encrypt su NPM
|
4. SSL Let's Encrypt su NPM
|
||||||
5. Aggiorna `infra/.env` sul server:
|
5. Aggiorna `infra/.env` sul server:
|
||||||
- `APP_PUBLIC_URL=https://www.matchlivetv.it`
|
- `APP_PUBLIC_URL=https://www.matchlivetv.it`
|
||||||
|
|||||||
Reference in New Issue
Block a user