Skip to content

Redis contract (internals)

Every message is one JSON envelope:

{ "type": "event", "match_id": "match_…", "session_id": "ses_…", "ts": 1760012145.32, "seq": 42, "data": { } }

type is one of status | frame | ball | players | event | metrics | error.

Key / channel Kind Writer → reader Purpose
worker:commands stream, group workers api → worker start commands ({command, match_id, session_id, config})
match:{id}:control string api → worker desired state {session_id, state: running|paused|stopped}
match:{id}:lease string, TTL 8 s worker → api {worker_id, session_id}, refreshed every second
worker:{id}:heartbeat string, TTL 10 s worker → api device, GPU metrics, models, active matches, capacity
status:updates stream, group api-status worker → api session status changes (persisted to Postgres)
events:stream stream, group api-persist worker → api events (persisted, idempotent on event_id)
match:{id}:status pub/sub worker, api → WebSocket status and error messages
match:{id}:events pub/sub worker → WebSocket event messages
match:{id}:detections pub/sub worker → WebSocket frame messages (ball, trail, players)
match:{id}:metrics pub/sub worker → WebSocket metrics once per second
match:{id}:frame:last, metrics:last string, TTL 30 s worker → api snapshot for newly connected clients

Worker session statuses: starting, processing, reconnecting, paused, completed, stopped, failed. The API maps them to match statuses (stopped becomes completed, reconnecting becomes processing) and ignores updates from superseded sessions or ones that contradict the current desired state.

On connect the server sends a snapshot (status, then frame, ball and players while the match is active, metrics, and the last 50 events), then relays the pub/sub channels above. Unknown match ids close with code 4404. If Redis is unavailable the server sends an error message and closes with 1011; clients reconnect with exponential backoff. Clients may send ping text frames; their content is ignored.

frame.data:

{
"frame_number": 1221, "video_timestamp": 122.1, "timestamp": 1760012145.32,
"width": 1280, "height": 720, "coordinate_mode": "pitch",
"ball": { "track_id": 1, "bbox": [836, 385, 848, 397], "pixel": {"x": 842, "y": 391},
"pitch": {"x": 72.4, "y": 38.1}, "confidence": 0.94,
"velocity": {"x": 120.5, "y": -14.2}, "speed": 121.3, "predicted": false },
"trail": [ {"t": 121.2, "x": 820.1, "y": 395.0, "pitch": {"x": 70.9, "y": 38.6}} ],
"players": [ {"track_id": 42, "class": "player", "bbox": [210, 240, 268, 410],
"confidence": 0.93, "pitch": {"x": 31.0, "y": 55.2}} ],
"latency_ms": 183.0
}