# Spectator studio

> How the games spectator studio builds its on-air panel, eligible list, bitrate ladder, and viewer figures from the broadcast records the control plane returns.

The spectator studio is the games console screen that turns a running
match into a broadcast you can put on air. Everything it draws comes
from one query: `fleetGames.spectatorStudio({ orgId })`, which returns a
`broadcasts` array. The screen renders those records directly — it does
not synthesise stream names, viewer counts, or ladder rungs when a field
is absent.

This page documents the shape of a broadcast record, how each panel
reads it, and what the screen does when a field is missing.

## What a broadcast record contains

Each entry in `broadcasts` maps to one row in the **Eligible to
promote** list and, when selected, to the **On air** panel. The screen
accepts two names for several fields and takes whichever is present.

| Source field | Accepted alias | Used for |
| ------------ | -------------- | -------- |
| `id` | — | The broadcast id. Also the row key when `match_id` is absent. |
| `match_id` | falls back to `id` | The identifier shown in the row's caption and used to track which row is on air. |
| `moq_namespace` | — | The MoQT namespace the broadcast publishes under. Also the fallback stream name. |
| `featured` | — | Coerced to a boolean on the record. |
| `status` | — | Drives the status pip. `active` is mapped to `ok`; any other value is passed through unchanged. |
| `mode` | — | Shown in the row title and the on-air hint. |
| `region` | — | Shown in the row title and the on-air hint. |
| `players` | — | Player count in the row caption (`… pl`). |
| `viewer_count` | — | Spectator count. Coerced with `Number()`. |
| `ladder` | `bitrate_ladder` | The rung list for the bitrate ladder panel. Defaults to an empty array. |
| `latency` | `time_to_glass_ms` | Time-to-glass, rendered in milliseconds under the stream frame. |
| `stream_name` | `moq_namespace` | The stream identifier printed on the stream frame. |
| `label` | `quality_label` | The caption on the stream frame. |

A ladder rung is its own small record:

| Rung field | Accepted aliases | Used for |
| ---------- | ---------------- | -------- |
| `label` | `resolution` | The rung name in the left column. |
| `fill_pct` | `fillPct` | The width of the rung's fill bar. |
| `bitrate_mbps` | `mbps` | The `… Mbps` figure. |
| `viewers` | `viewer_count` | The viewer figure at the right of the row. |

## How a broadcast becomes eligible to promote

The **Eligible to promote** list is the `broadcasts` array, unfiltered
and in the order the control plane returned it. There is no client-side
filter or sort step: if a broadcast is in the response, it is in the
list. The `Mode` filter chip in the panel header is rendered in its
inactive state and is not wired to a filter.

If the response contains no broadcasts, the panel shows *No broadcasts
eligible for promotion.* rather than an empty box.

## What promoting a broadcast does

The **Promote** button sets the screen's local selection to that
broadcast's match id. That selection controls which record feeds the On
air panel and which row shows the `ON AIR` tag instead of a Promote
button. It issues no mutation — nothing about the broadcast changes in
ClutchCall's control plane when you promote from this screen, and the
selection resets when the screen unmounts.

If nothing has been selected yet, the On air panel falls back to the
first record in `broadcasts`. If the array is empty, the panel renders
its empty state: hint *No active broadcast*, no LIVE tag, and a stream
frame that is not marked live.

The **Cut**, **Cut to replay**, **Featured carousel**, and **Add slot**
controls are present but disabled on this screen.

## Reading the on air panel

The panel header hint reads `mode · region · <viewers> viewers`, where
the viewer figure is the record's `viewer_count` rendered with
`toLocaleString()`. A LIVE tag appears whenever a broadcast is selected.

The stream frame below it shows three values taken straight from the
record:

- **Label** — `label`, or `quality_label`, or the literal
  `Live broadcast` when a broadcast is selected but neither field is set.
- **Stream** — `stream_name`, or `moq_namespace`.
- **Time-to-glass** — `latency` or `time_to_glass_ms`, printed as
  `<n> ms time-to-glass`.

The screen does not measure time-to-glass itself. It prints the number
the broadcast record carries, in milliseconds, with no rounding or unit
conversion.

## The bitrate ladder

Under the stream frame, each rung of the selected broadcast's ladder
renders as one row: rung name, a fill bar, the rung bitrate in Mbps, and
the rung's viewer figure.

The fill bar's width is the rung's `fill_pct` as reported by the control
plane. The screen clamps it into the range 0–100 and applies no other
transformation — it does not compute fill from the viewer counts or the
bitrate. A rung whose `fill_pct` is absent renders as an empty bar.

Rungs are drawn in the order they appear in the array. The panel's
caption — *drop a rung to relieve egress* — describes the intent of the
ladder view; dropping a rung is not an action this screen performs.

When the selected broadcast has no `ladder` (or `bitrate_ladder`), or
the array is empty, the section shows *No bitrate ladder reported.*

## Viewer count and edge cache panels

Two panels sit below the carousel:

- **Viewer count · last 30 min** renders a fixed empty state, *No
  broadcast sessions yet.* The `spectatorStudio` response carries a
  per-broadcast `viewer_count`, but no viewer time series, so the chart
  has no series to draw.
- **Cache hit ratio · per PoP** renders one bar per point of presence
  from a list that is currently empty, so the panel draws no rows.

The **Featured carousel** and **Replay tray** panels behave the same
way: they iterate an empty list and therefore render no cards.

## When a field reports no value

The screen prefers a visible placeholder over an invented value, but the
placeholder differs by field:

| Field | Missing-value rendering |
| ----- | ----------------------- |
| `mode`, `region` | `—` |
| `players` | `—` in the row caption |
| `viewer_count` | `0` — both in the on-air hint and the row caption, because the count is defaulted before formatting |
| `latency` / `time_to_glass_ms` | `—` in place of the whole `… ms time-to-glass` string |
| `stream_name` / `moq_namespace` | `—` on the stream frame |
| `label` / `quality_label` | `Live broadcast` when a broadcast is selected, `No active broadcast` when none is |
| `status` | Treated as `ok` |
| rung `label`, `bitrate_mbps`, `viewers` | `—` in that column |
| rung `fill_pct` | Bar renders at zero width |

A viewer count of `0` in the UI therefore means either "no spectators"
or "the record omitted `viewer_count`". Check the raw response if the
distinction matters.

## Refresh cadence

The query refetches every 30 seconds while the screen is mounted, and
only runs once an org id is resolved. The header shows `Refreshing…`
during a fetch and otherwise `Updated <relative time>`, derived from
when the query data last changed. The refresh button forces a refetch
and is disabled while one is in flight.

Changing the selected broadcast does not refetch; a refresh keeps your
selection as long as that match id is still present in the new response,
and otherwise falls back to the first record.

## Related

- [Telemetry](/platform/telemetry) — the metric, trace, and CDR streams behind operational panels
- [Authentication](/concepts/authentication) — API keys for control-plane calls, relay tokens for MoQT namespaces
