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>
14 KiB
Tabelloni, overlay e regolamenti sportivi
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.
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 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 |
|---|---|---|---|
periods |
Numero di periodi regolamentari | 4 quarti | 2 tempi |
period_duration_secs |
Durata iniziale del cronometro per periodo | 600 (10 min) | 1200 (20 min) |
overtime_duration_secs |
Durata supplementare / OT | 300 (5 min) | 300 |
Sport timed: calcio a 5, pallamano, hockey indoor, floorball.
Il cronometro è gestito dal motore (clock_toggle, clock_reset, clock_tick, advance_period) e memorizzato in score_states.data (JSONB).
Sport senza punteggio a referto (board: timer o generic)
| Board | Uso | Regole | Overlay default |
|---|---|---|---|
timer |
Ginnastica, danza, cheerleading, pattinaggio | Nessuna ({}) |
none |
generic |
«Altro» | Nessuna | none |
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.
Scoring::Engine.for_match(match) seleziona l’engine:
| Board | Classe engine | Azioni principali |
|---|---|---|
volley |
VolleyEngine |
home_point, away_point, home_undo, away_undo, close_set |
racket |
RacketEngine |
Come volley (fase 1 — stessa logica a set) |
basket |
BasketEngine |
+1/+2/+3, undo, advance_period, cronometro |
timed |
TimedEngine |
+1, undo, periodi, cronometro |
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:
match.effective_scoring_rules
# => default_rules del catalogo deep_merge scoring_rules della partita
Quindi:
- Default dello sport — es. pallavolo: 25 punti, set decisivo a 15.
- Override partita — campo
matches.scoring_rules(JSON) per deroghe locali (categoria giovanile, torneo ridotto…). - Legacy — per il volley,
matches.sets_to_winviene fuso ineffective_scoring_rulesse 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
timercome overlay è mappato anoneinMatch#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_overlaysdello sport, altrimenti si ripiega sul default.
Esempio: il basket ammette [basket, none] — si può trasmettere con tabellone o solo watermark.
Layout app mobile (Android e iOS)
Sull’app nativa l’overlay è composito in GPU senza ricodifica aggiuntiva:
BroadcastScreencostruisce unOverlayStatea partire daeffectiveOverlayKindeScoreState.OverlayRenderer→OverlayCanvasRendererdisegna gli elementi su bitmap trasparente.- Il bitmap viene applicato come filtro sul flusso RTMP (OpenGL su Android, HaishinKit su iOS).
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.
Mappatura overlay ← punteggio
| Overlay | Struttura dati overlay | Fonte dati |
|---|---|---|
volley / racket |
ScoreboardState (colonne set) |
set_partials, current_set, punti correnti |
basket / timed |
CompactScoreboardState |
home_score / away_score in data, periodLabel |
none |
Solo watermark / stato LIVE opzionale | — |
Stato tabellone (ScoreState)
Ogni StreamSession ha al massimo uno ScoreState, creato all’apertura sessione (Sessions::Create → Scoring::Engine.ensure_score_for).
Campi condivisi
| Campo | Uso |
|---|---|
board_type |
Snapshot del board al momento della creazione |
home_sets / away_sets |
Set vinti (volley/racket) |
home_points / away_points |
Punti set corrente oppure mirror del punteggio basket/timed |
current_set |
Set in corso |
set_partials |
Storico parziali [{set, home, away}, …] |
data (JSONB) |
Stato esteso basket/timed/timer |
Payload WebSocket
ScoreState#as_cable_payload arricchisce il JSON in base al board:
- volley / racket: set, punti, parziali.
- basket / timed:
home_points/away_pointscome punteggio totale,period,clock_secs,clock_running. - timer: solo cronometro.
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.
Flusso end-to-end
1. Creazione partita
team.sport_key → match.sport_key
opzionale: match.overlay_kind, match.scoring_rules
2. Avvio sessione (POST /sessions)
→ ScoreState inizializzato con default_data dell’engine
→ Per basket/timed: clock_secs = period_duration_secs del regolamento
3. Azione operatore (app, regia, API)
POST score_action / WebSocket
→ Scoring::ApplyAction → Engine.apply
→ ScoreState aggiornato + broadcast Action Cable
4. Rendering overlay (solo app camera / relay grafico)
ScoreState + Match.effectiveOverlayKind
→ OverlayState → bitmap → filtro GL sul video
5. Spettatori
Vedono il video con overlay già composito (non ricevono il tabellone come layer separato)
Tabella sport → board → overlay → regolamento
| sport_key | Board | Overlay default | Regolamento chiave |
|---|---|---|---|
| pallavolo | volley | volley | 3 set, 25 pt, 5° set a 15, +2 |
| beach_volley | volley | volley | 2 set, 21 pt, 3° set a 15 |
| sitting_volley | volley | volley | Come pallavolo |
| basket | basket | basket | 4×10 min, OT 5 min |
| calcio_a_5 | timed | timed | 2×20 min, suppl. 5 min |
| pallamano | timed | timed | 2×30 min |
| hockey_indoor | timed | timed | 2×20 min |
| floorball | timed | timed | 3×20 min |
| tennis | racket | racket | 2 set, 6 game, tie-break 7 |
| tennis_tavolo | racket | racket | 3 set, 11 pt |
| badminton | racket | racket | 2 set, 21 pt |
| padel | racket | racket | Come tennis |
| pickleball | racket | racket | 2 set, 11 pt |
| ginnastica_* / danza / cheer / pattinaggio | timer | none | Nessun punteggio |
| altro | generic | none | Contatore libero |
Personalizzazione per partita
| Campo DB | Effetto |
|---|---|
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:
{
"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 |
| Validazione regole | backend/app/domain/sports/rules_schema.rb |
| Logica regolamento | backend/app/services/scoring/rules.rb |
| Engine | backend/app/services/scoring/engine.rb, engines/* |
| Modello partita | backend/app/models/match.rb |
| Stato punteggio | backend/app/models/score_state.rb |
| Overlay Android | native/android/.../streaming/overlay/* |
| Overlay iOS | native/ios/MatchLiveTv/Streaming/Overlay/* |
| Schermata broadcast Android | native/android/.../ui/broadcast/BroadcastScreen.kt |
| Schermata broadcast iOS | native/ios/MatchLiveTv/UI/Broadcast/BroadcastScreen.swift |
| Regia web | backend/app/views/public/regia/_*.html.erb |
Documento correlato: MULTI_SPORT.md (panoramica più sintetica della migrazione multi-sport).
Limitazioni attuali
racket=volleya livello motore — tennis/padel usano logica a set+punti numerici; non c’è ancora scoring «15-30-40» o tie-break automatico stile tennis.- Cronometro non in overlay — per basket/timed il tempo è gestito in regia ma non bruciato sul video.
- Overlay solo lato publisher — lo spettatore vede il grafico composito nel flusso; non c’è tabellone HTML separato sulla pagina live pubblica.
- 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.