The latency console plays one timecoded source over three transports at the same time and reports the glass-to-glass delay of each. It is the screen you open when someone asks “how much faster is MoQT than HLS on this network, right now, for this stream?” The screen is a thin wrapper. The panels and the measurement logic come from a framework-light (pure DOM) console module that ClutchCall shares with the public demo console, so the console in the operator app and the console on the demo page are the same implementation.

What the console compares

Up to three panels, one per delivery path: Panels are independent. Configure one, two, or all three. A panel that has no endpoint is simply not mounted — the MoQT panel in particular is skipped unless both the relay URL and the namespace are present. If none of the four fields is set, the console does not mount at all and the screen shows a hint instead. The comparison is only meaningful when all configured endpoints carry the same timecoded source (the same encoder output, published to the relay and to a mediamtx-style WHEP/HLS origin). Nothing in the console verifies that; pointing the panels at different sources produces three unrelated numbers.

Point it at a relay, WHEP and HLS endpoint

The four endpoints live in the URL query string, and the inline form at the top of the screen writes them there:
Pressing Load validates the form, then rewrites location.search with the non-empty fields, which reloads the screen. Because the endpoints come from the URL, an operator can aim the console at any relay plus WHEP/HLS origin without a backend change, and a working configuration is a shareable link. Whenever the query endpoints change, the console module is destroyed and re-mounted, and playback restarts on all panels together.

Validation

Validation is reveal-on-submit: errors appear only after you press Load. This exists because invalid values used to be written straight into location.search, after which the console silently never mounted and the screen looked broken. The rules enforced:
  • A namespace without a relay URL, or a relay URL without a namespace, is an error — the MoQT panel needs both.
  • relay must be a URL with the https protocol, because it is a WebTransport URL.
  • whep and hls must be valid http or https URLs.
  • If all four fields are empty, the form reports that you need a relay + namespace, a WHEP URL, or an HLS URL before anything can load.

Finding the relay URL and namespace for a live input

The form is not prefilled today — you paste both values. (Prefilling them from a selected live input through the BFF is a known follow-up, not something this screen does yet.)
  • Relay URL is the WebTransport endpoint of the relay deployment serving the stream, on its QUIC :443 plane, including the MoQT path — the placeholder shows the shape: https://relay.host:443/moq.
  • Namespace is the namespace the live input publishes under. The placeholder shows the shape: stream/tenant/lv_xxx — the tenant, then the live input id.
Paste the same namespace the publisher uses. A namespace that nothing is publishing to yields an empty MoQT panel while WHEP and HLS keep playing, which is the usual cause of a “MoQT is broken” report.

How glass-to-glass is measured

The source carries a barcode burned into the video pixels that encodes the capture timecode. Each panel decodes and paints frames as normal, the console reads the barcode back out of the painted pixels, and the delay is the difference between the timecode it just read and now. The consequence worth understanding: this measures the pixels the viewer actually sees, not a protocol-level timestamp. Everything between the moment the barcode was burned in and the moment the frame is painted is included — packaging, the transport hop, relay or origin buffering, player jitter buffers and playlist/segment buffering, and decode. Anything upstream of the burn-in — sensor capture, the camera pipeline, and any processing before the timecode was stamped — is not included, and is identical across the three panels anyway. That is what makes the three numbers comparable: the same burn-in feeds all three, so the differences between panels are attributable to the delivery path and its player, not to the capture side.

Reading the three panels

Each configured panel renders its own player plus its own live glass-to-glass readout, so the three sit side by side on one screen against one source. Read them as a relative result for the network and stream you are pointed at. The absolute figure depends on the encoder, the origin, the buffering policy of each player, and the path between that machine and the relay or origin — a console run from a different network is a different measurement, not a regression. A panel that stays blank while the others play is a per-path problem: an unpublished or misspelled namespace and a blocked QUIC path affect only MoQT; a WHEP or HLS origin that is not serving the stream affects only that panel.

QUIC-blocked networks and the WS fallback build

The MoQT panel uses the default client build, which is WebTransport over QUIC against the relay’s :443 plane (/sdk/moqt.js). That is the product path and it is what this console measures. A WebSocket-fallback MoQT build also exists (/sdk/moqt-ws.js) for networks where QUIC/UDP is blocked. It is not selectable from this screen — the console always mounts the WebTransport build. So on a QUIC-blocked network the MoQT panel will fail to connect here even though a fallback build would play; that is a property of this console, not of the stream. If the MoQT panel is the only one failing and the relay is otherwise healthy, suspect UDP egress before you suspect the publisher.

Where the console runs

The shared console module treats hls.js as an external dependency and, by default, loads it from /vendor/hls.mjs. That default is correct for the standalone console build served under /console/ on the streams host, which ships its own vendor directory. Inside the operator SPA that path is not a real asset — the dev server would try to resolve it as a source module and fail — so this screen hands the module the URL of the hls.js the package already depends on. Both deployments end up running the same panels and the same measurement; only the hls.js source differs.

Limitations

  • No prefill. Relay, namespace, WHEP and HLS are typed or pasted by hand. There is no picker that resolves them from a selected live input.
  • No source verification. The console assumes the endpoints carry the same timecoded, barcode-burned source. It cannot tell you that they do not.
  • Barcode required. Without the burned-in timecode there is no glass-to-glass reading; the panels would still play, but the measurement has nothing to decode.
  • WebTransport only for MoQT. The WS fallback build is not reachable from this screen.
  • https relay only. A relay URL on any other scheme is rejected by the form.
  • Applying endpoints reloads the screen. Load rewrites the query string, so all panels restart; it is not an incremental update of one panel.
  • Point-in-time, client-side. The numbers are measured by the browser you are sitting at. Nothing is recorded or persisted by this screen.