Evita il riempimento disco da segmenti MediaMTX post-sessione con cron orario;
documenta come board, overlay e regolamenti sportivi si relazionano nel sistema.
Co-authored-by: Cursor <cursoragent@cursor.com>
Questo documento descrive come Match Live TV modella gli sport, applica i regolamenti di punteggio e li traduce in **tabelloni** (logica + controlli) e **overlay** (grafica sul video in diretta).
---
## Panoramica
Il sistema separa tre livelli che spesso vengono confusi:
| Livello | Cosa rappresenta | Dove vive | Esempio basket |
| **Sport** (`sport_key`) | La disciplina scelta per squadra/partita | `config/sports.yml`, DB | `basket` |
| **Board** (`board_type`) | Il *tipo di tabellone* — logica di punteggio e controlli | Derivato dal catalogo, `score_states.board_type` | `basket` |
| **Overlay** (`overlay_kind`) | Il *layout grafico* sul video | Per partita, validato dal catalogo | `basket` o `none` |
In sintesi:
- Il **regolamento** definisce *come si segna* (set da vincere, punti per set, durata quarti…).
- Il **board** è il motore che applica quel regolamento.
- L’**overlay** è solo la resa visiva sullo stream; può essere disattivato (`none`) anche quando il board resta attivo in regia.
```mermaid
flowchart LR
subgraph catalogo [Catalogo sport]
SK[sport_key]
BR[board_type]
OV[overlay default]
DR[default_rules]
end
subgraph partita [Partita]
M[Match]
OK[overlay_kind opzionale]
SR[scoring_rules opzionale]
end
subgraph runtime [Sessione live]
SS[ScoreState]
ENG[Scoring Engine]
OL[Overlay video]
end
SK --> BR
SK --> OV
SK --> DR
M --> SK
M --> OK
M --> SR
BR --> ENG
DR --> ENG
SR --> ENG
ENG --> SS
SS --> OL
OK --> OL
```
---
## Catalogo sport e regolamenti
Il catalogo è definito in [`backend/config/sports.yml`](../backend/config/sports.yml) e caricato da `Sports::Catalog`. Non esiste una tabella `sports` nel database: lo sport è una **configurazione versionata** esposta via API (`GET /api/v1/sports`).
Per ogni disciplina il catalogo specifica:
| Campo | Significato |
|-------|-------------|
| `label` | Nome visualizzato (es. «Pallavolo») |
| `board` | Tipo tabellone implicito |
| `overlay` | Overlay video predefinito |
| `allowed_overlays` | Overlay ammessi sulla partita |
| `default_rules` | Regolamento standard di quello sport |
### Tipi di regolamento (`default_rules`)
Gli sport si raggruppano in tre famiglie di regole, validate da `Sports::RulesSchema`:
#### Sport a set e punti (`board: volley` o `racket`)
| Chiave | Significato | Esempio pallavolo |
|--------|-------------|-------------------|
| `sets_to_win` | Set necessari per vincere la partita | 3 (best of 5) |
| `points_per_set` | Punti per vincere un set «normale» | 25 |
| `points_deciding_set` | Punti dell’ultimo set (tie-break) | 15 |
| `min_point_lead` | Vantaggio minimo per chiudere il set | 2 |
**Sport che usano queste regole:** pallavolo, beach volley, sitting volley, tennis, tennis tavolo, badminton, padel, pickleball.
La logica è in `Scoring::Rules` (`set_winner_from_points`, `match_winner`): un set si vince al raggiungimento del target (`points_per_set` o `points_deciding_set` sull’ultimo set possibile) **con almeno**`min_point_lead` punti di distacco.
#### Sport a tempi e periodi (`board: basket` o `timed`)
| Chiave | Significato | Esempio basket | Esempio calcio a 5 |
Per questi sport la regia può usare un cronometro (`TimerEngine`) ma **non** viene sovrapposto un tabellone sul video.
---
## Board type: il motore di punteggio
Il `board_type` non si sceglie manualmente in creazione partita: deriva sempre dallo `sport_key` tramite `Match#effective_board_type` → `Sports::Catalog.board_for`.
| `timer` | `TimerEngine` | Solo cronometro (conteggio crescente) |
| `generic` | `GenericEngine` | `+1` / undo senza chiusura set |
### Come il regolamento entra nel motore
All’avvio di ogni azione, l’engine costruisce le regole effettive con:
```ruby
match.effective_scoring_rules
# => default_rules del catalogo deep_merge scoring_rules della partita
```
Quindi:
1.**Default dello sport** — es. pallavolo: 25 punti, set decisivo a 15.
2.**Override partita** — campo `matches.scoring_rules` (JSON) per deroghe locali (categoria giovanile, torneo ridotto…).
3.**Legacy** — per il volley, `matches.sets_to_win` viene fuso in `effective_scoring_rules` se presente.
Il volley applica automaticamente la vittoria di set quando il punteggio soddisfa `Rules#set_winner_from_points`; `close_set` forza la chiusura manuale (utile se si preferisce confermare a bordo campo).
---
## Overlay kind: la grafica sul video
L’overlay è **indipendente** dal motore di scoring: descrive *cosa disegnare* sul frame, non *come calcolare* i punti.
### Valori ammessi
Definiti in `Sports::OverlayKind`: `none`, `volley`, `basket`, `timed`, `racket`.
> Il valore legacy `timer` come overlay è mappato a `none` in `Match#effective_overlay_kind`.
### Scelta per partita
- Campo opzionale `matches.overlay_kind`.
- Se assente → default del catalogo (`Sports::Catalog.overlay_for`).
- Deve essere incluso in `allowed_overlays` dello sport, altrimenti si ripiega sul default.
Esempio: il basket ammette `[basket, none]` — si può trasmettere con tabellone o solo watermark.
### Layout Android (pipeline video)
Sull’app nativa l’overlay è composito in GPU senza ricodifica aggiuntiva:
1.`BroadcastScreen` costruisce un `OverlayState` a partire da `effectiveOverlayKind` e `ScoreState`.
2.`OverlayRenderer` → `OverlayCanvasRenderer` disegna gli elementi su bitmap trasparente.
3. Il bitmap viene applicato come filtro OpenGL sul flusso RTMP.
Elementi grafici (`OverlayCanvasRenderer`):
| Elemento | Quando è attivo |
|----------|-----------------|
| `WatermarkElement` | Sempre tranne `overlay_kind = none` |
| `ScoreboardElement` | `volley`, `racket` — tabellone a colonne per set |
| `CompactScoreboardElement` | `basket`, `timed` — squadre ai lati, punteggio centrale, etichetta periodo |
| `SponsorElement` | Se configurato |
**Nota:** il cronometro non è più renderizzato nell’overlay video (basket/timed mostrano solo periodo, es. `Q2` o `2° tempo`). Il cronometro resta disponibile nella **regia** e nei controlli operatore, non sullo stream.
L’app mobile e la regia web si sottoscrivono al canale `SessionChannel` e ricevono `score_update` dopo ogni azione.
---
## Regia web (controlli operatore)
La pagina regia (`public/regia`) usa lo stesso motore di scoring dell’API, con partial diversi per board:
| Partial | Board | Controlli tipici |
|---------|-------|------------------|
| `_volley.html.erb` | volley | +1, undo, chiudi set, hint punti target |
| `_racket.html.erb` | racket | Come volley |
| `_basket.html.erb` | basket | +1/+2/+3, cronometro, avanza quarto |
| `_timed.html.erb` | timed | +1, cronometro, avanza tempo |
| `_timer.html.erb` | timer | Solo cronometro |
| `_generic.html.erb` | generic | Contatore punti semplice |
Il partial viene scelto in base a `data-board` sul container `#regia-app`. I pulsanti inviano azioni al controller regia, che chiama `Scoring::ApplyAction`.
| `sport_key` | Determina board, overlay default e regole base |
| `overlay_kind` | Cambia solo la grafica sul video (entro `allowed_overlays`) |
| `scoring_rules` | Sovrascrive i numeri del regolamento (es. `points_per_set: 21` su pallavolo giovanile) |
| `sets_to_win` | Legacy; fuso in `effective_scoring_rules` per il volley |
Esempio API partita:
```json
{
"sport_key":"pallavolo",
"overlay_kind":"none",
"scoring_rules":{
"points_per_set":21,
"sets_to_win":2
}
}
```
→ Regia e motore usano regole da 21 punti; lo stream non mostra tabellone (`none`).
---
## Relazione regolamento ↔ UI ↔ overlay
| Aspetto | Governato da | Visibile in overlay? |
|---------|--------------|----------------------|
| Vittoria set (25+2) | `Scoring::Rules` + `VolleyEngine` | Sì — colonne set / parziali |
| Vittoria partita (3 set) | `match_winner` | Sì — conteggio set |
| Punti basket (+2/+3) | `BasketEngine` | Sì — punteggio centrale |
| Durata quarto | `period_duration_secs` | No — solo etichetta periodo (`Q1`…) |
| Cronometro | `score_states.data` | No sull’overlay (sì in regia) |
| Sport ginnico | `TimerEngine` | No overlay punteggio |
Questa separazione è intenzionale: il regolamento alimenta sempre il **modello dati** (`ScoreState`); l’**overlay** sceglie quali campi mostrare sul video.
---
## File di riferimento
| Area | Percorso |
|------|----------|
| Catalogo sport | `backend/config/sports.yml` |
| API catalogo | `backend/app/domain/sports/catalog.rb` |
| Regia web | `backend/app/views/public/regia/_*.html.erb` |
Documento correlato: [`MULTI_SPORT.md`](./MULTI_SPORT.md) (panoramica più sintetica della migrazione multi-sport).
---
## Limitazioni attuali
1.**`racket` = `volley` a livello motore** — tennis/padel usano logica a set+punti numerici; non c’è ancora scoring «15-30-40» o tie-break automatico stile tennis.
2.**Cronometro non in overlay** — per basket/timed il tempo è gestito in regia ma non bruciato sul video.
3.**Overlay solo lato publisher** — lo spettatore vede il grafico composito nel flusso; non c’è tabellone HTML separato sulla pagina live pubblica.
4.**Regolamenti = parametri numerici** — il sistema non incorpora testi di regolamento FIPAV/FIBA; implementa le regole standard dichiarate in `sports.yml`, personalizzabili per partita.
Per aggiungere uno sport nuovo: estendere `sports.yml`, verificare che `board` e `allowed_overlays` siano coerenti, e — se serve — aggiungere un partial regia e un ramo in `BroadcastScreen` / `OverlayCanvasRenderer`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.