The wire format for GalaSpo Stream's burned-in telemetry strips. Spec version 1.0 — last matched to the codec source on 2026-09-13.
Every broadcast GalaSpo Stream produces carries a second, silent channel: five short black/grey strips burned into the pixels along the bottom edge of the frame, encoding camera pose and match status directly into the video itself. No sidecar file, no metadata track — the recording is the data. This page is that format's single specification.
CameraDataStripCodec.swift and
GameDataStripCodec.swift implement, described in prose and tables for a person
and repeated as a single structured block for a program. Fetch this page and read the
<script id="machine-code-spec" type="application/json"> block near the end
to get the same facts without re-deriving them from prose — a person reading the page above
it and a model parsing the JSON below it are working from the same specification, not two
independent descriptions of it.
Five independent strips are burned into every frame: four short bars in the two bottom corners carrying the camera's attitude (yaw, pitch, roll) and field of view, plus one longer bar centred on the bottom edge carrying the match clock and the last scoring or disciplinary event. Each strip is its own self-contained guard/payload/CRC frame — losing one doesn't affect the others.
Cells alternate between two colors chosen to be almost invisible to a viewer but trivially
separable by a decoder that thresholds locally instead of against an absolute brightness:
black = 0 and mid-grey (white 0.5) = 1.
The strips survive re-encoding — including a round trip through YouTube's H.264 pipeline —
because every cell boundary is snapped to an exact integer pixel count at render time (see
Frame geometry), so nearest-neighbour scaling never blurs a cell edge.
Every strip, regardless of payload, shares the same frame shape:
// [ guard 3 ] [ payload N bits ] [ CRC-5 ] [ guard 3 ]
[0 1 0] payload… crc(5b) [0 1 0]
[0, 1, 0] (black/grey/black), present at both the start and the end of the strip. A decoder confirms both guards match before trusting anything in between.Both strip families use the identical checksum: polynomial 0x05 (x⁵+x²+1), initial value 0x1F, no final XOR, computed MSB-first over the exact payload bit sequence (not bytes).
// crc: 5-bit register, seeded 0x1F crc = 0x1F for bit in payload: // MSB-first mix = ((crc >> 4) & 1) XOR bit crc = (crc << 1) & 0x1F if mix == 1: crc = crc XOR 0x05 // final crc (5 bits) is appended after the payload, before the end guard
Four strips, one field each, all built from a shared field shape: a linear range, a
0.1° step, and 12 bits (4096 levels, of which the top one is reserved). A field's raw value
is quantized as level = round((clamp(value, lo, hi) − lo) / step), clamped to
0 … 2ᵇⁱᵗˢ − 2. The all-ones level (2ᵇⁱᵗˢ − 1 = 4095) is the sentinel
for "no reading" — decodes to null, not a value near the range's edge.
| Segment | Edge | Field | Range | Step | Bits | Levels |
|---|---|---|---|---|---|---|
bottomLeft | bottom edge, aligned left | yaw | −180° … 180° | 0.1° | 12 | 3600 used / 4096 |
left | left edge, aligned bottom | roll | −180° … 180° | 0.1° | 12 | 3600 used / 4096 |
bottomRight | bottom edge, aligned right | pitch | −180° … 180° | 0.1° | 12 | 3600 used / 4096 |
right | right edge, aligned bottom | fov | 0° … 180° | 0.1° | 12 | 1800 used / 4096 |
Every camera strip is therefore 23 cells total: 3 guard + 12 payload +
5 CRC + 3 guard. Field of view is deliberately specified over the full 0–180° range — wider
than any single lens reports — so the wire format stays device-independent while every bar
still lays out to the same cell count. left and right run
top-to-bottom rather than left-to-right; everything else about their frame is identical.
Rendered at 30 Hz from live gimbal/AVFoundation attitude readings; a field reads as
null whenever the underlying sensor has no reading yet, not just when it's
genuinely at the sentinel level.
One strip, centred on the bottom edge, packing five fields into a 34-bit payload — 45 cells total (3 guard + 34 payload + 5 CRC + 3 guard). Unlike the camera fields, these are plain unsigned integers, not quantized ranges.
| Field | Bits | Range | Meaning |
|---|---|---|---|
gameTimeSeconds | 13 | 0 … 8191 | Whole seconds, continuous across halves. |
additionalTimeSeconds | 11 | 0 … 2047 | Stoppage time past the scheduled end of the current half. |
situationType | 4 | 0 … 9 (enum) | See Situation below; 0 = none. |
situationTeam | 2 | 0 … 2 (enum) | See Team below. |
situationSeq | 4 | 0 … 15, wraps | Increments on every trigger — including the return to "none" — so two back-to-back identical events are distinguishable frame to frame. |
| Value | Name |
|---|---|
| 0 | none |
| 1 | goalAdded |
| 2 | goalSubtracted |
| 3 | penaltyGoal |
| 4 | penaltyMiss |
| 5 | penaltyOutcomeSubtracted |
| 6 | foul |
| 7 | offside |
| 8 | yellowCard |
| 9 | redCard |
| Value | Name |
|---|---|
| 0 | unspecified |
| 1 | home |
| 2 | away |
Strips are sized as an exact multiple of an integer cell so nearest-neighbour scaling never lands a cell boundary mid-pixel — this is what lets them survive a lossy re-encode:
stripThicknessPx = max(3, round(frameHeight * 0.003)) cellSizePx = max(2, round(frameHeight / 180)) // 4px@720p · 6px@1080p · 8px@1440p · 12px@4K barLengthPx = cellSizePx * segmentCellCount // 23 for a camera strip, 45 for the game strip
Placement, in frame pixels (Core Image convention: y = 0 is the visual bottom edge):
| Strip | x | y | Size |
|---|---|---|---|
bottomLeft | 0 | 0 | barLength × stripThickness |
bottomRight | frameWidth − barLength | 0 | barLength × stripThickness |
left | 0 | stripThickness | stripThickness × barLength |
right | frameWidth − stripThickness | stripThickness | stripThickness × barLength |
gameStatus | round((frameWidth − gameBarLength) / 2) | 0 | gameBarLength × stripThickness |
The vertical strips (left, right) sit one strip-thickness up from
the bottom so they never overlap the horizontal strips in the corners. All five strips are
composited as the literal outermost pixels of the frame — nothing else is drawn under them.
A decoder needs no metadata beyond the frame's own pixel dimensions — every strip's position
and cell size are pure functions of frameWidth/frameHeight:
frameHeight using the formulas above.0 and known-1 positions — rather than any fixed absolute brightness, since re-encoding shifts levels.[0, 1, 0] (checked in both directions — a strip carries no separate directionality marker beyond the guard match itself).payload (the field's bit width) and the trailing 5-bit crc.payload; reject the frame on any mismatch.payload MSB-first as an unsigned integer. For a camera field: the sentinel level (2ᵇⁱᵗˢ − 1) means null; otherwise value = lo + level * step. For the game strip: split the 34-bit payload into its five fixed-width fields in the order listed above and look up the two enums.Every strip is decoded independently — a CRC failure on one (occlusion, a compression artifact, a UI element drawn over a corner) doesn't invalidate the others.
The block below is the same specification as the prose above, as data — pull it out with a DOM query or a simple regex over the page source rather than re-transcribing the tables.
This page mirrors the codec's actual source. If the two ever disagree, the code wins — these are the canonical files: