GalaSpo Camera Code

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.

For humans and for LLMs. Everything below — the tables, the CRC, the pixel geometry — is exactly what 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.

Contents

Overview

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.

Cell encoding

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]

CRC-5/USB

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

Camera-pose strips

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.

SegmentEdgeFieldRangeStepBitsLevels
bottomLeftbottom edge, aligned leftyaw−180° … 180°0.1°123600 used / 4096
leftleft edge, aligned bottomroll−180° … 180°0.1°123600 used / 4096
bottomRightbottom edge, aligned rightpitch−180° … 180°0.1°123600 used / 4096
rightright edge, aligned bottomfov0° … 180°0.1°121800 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.

Game-status strip

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.

FieldBitsRangeMeaning
gameTimeSeconds130 … 8191Whole seconds, continuous across halves.
additionalTimeSeconds110 … 2047Stoppage time past the scheduled end of the current half.
situationType40 … 9 (enum)See Situation below; 0 = none.
situationTeam20 … 2 (enum)See Team below.
situationSeq40 … 15, wrapsIncrements on every trigger — including the return to "none" — so two back-to-back identical events are distinguishable frame to frame.

Situation enum

ValueName
0none
1goalAdded
2goalSubtracted
3penaltyGoal
4penaltyMiss
5penaltyOutcomeSubtracted
6foul
7offside
8yellowCard
9redCard

Team enum

ValueName
0unspecified
1home
2away

Frame geometry & placement

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):

StripxySize
bottomLeft00barLength × stripThickness
bottomRightframeWidth − barLength0barLength × stripThickness
left0stripThicknessstripThickness × barLength
rightframeWidth − stripThicknessstripThicknessstripThickness × barLength
gameStatusround((frameWidth − gameBarLength) / 2)0gameBarLength × 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.

Decode algorithm

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:

  1. Compute each strip's expected pixel rectangle from frameHeight using the formulas above.
  2. Sample one luminance value per cell along the strip's axis (its centre, ideally with a small median filter to absorb encoder noise).
  3. Derive a local dark/light threshold from the sample itself — e.g. the median of the guard cells' known-0 and known-1 positions — rather than any fixed absolute brightness, since re-encoding shifts levels.
  4. Threshold every cell against it to get a bit array; confirm the first 3 and last 3 bits equal the guard [0, 1, 0] (checked in both directions — a strip carries no separate directionality marker beyond the guard match itself).
  5. Split the interior into payload (the field's bit width) and the trailing 5-bit crc.
  6. Recompute CRC-5/USB over payload; reject the frame on any mismatch.
  7. Read 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.

Machine-readable spec

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.


  

  

Source of truth

This page mirrors the codec's actual source. If the two ever disagree, the code wins — these are the canonical files: