Video Utils Runner
The Video Utils runner (video-utils) turns the H.264 video that Capture streams inside its protobuf payloads into something the HEAT dashboard can play back in sync with the session clock. Its main template, video-atlas-encode, composites every admitted headset stream into one atlas (an N-up grid) and publishes it as a ladder of Media Source Extensions (MSE) fragments plus a manifest.
Read this before adding video to a session template: the output is not a downloadable video file, and the node has to sit at a specific place in the DAG.
What the output is, and is not
- The atlas is an MSE fragment ladder: one init segment plus one fragmented-MP4 media segment per time window (8 seconds by default), described by a JSON manifest. The dashboard’s
VideoPlaybackcomponent fetches fragments on demand and feeds them to aMediaSourcebuffer, seeks by window, and follows the realm clock. - There is no single MP4 to download and play in a desktop player. Individual fragments are not self-contained files, the manifest is the only thing that gives them meaning, and the windows can be rewritten in place as data arrives. If a downloadable recording is a requirement, that is a separate export feature, not this node.
- Playback is progressive and contiguous by design: every window between the first and last carries media, and a seat with no data in a window is rendered black rather than leaving a hole, because a hole in an MSE buffer stalls the browser rather than showing “no video”.
Node template selection
| Template | Purpose | Limitations | Details |
|---|---|---|---|
video-atlas-encode | Decode admitted StreamComponent H.264 streams straight from the capture protobuf blobs, composite them into one atlas, and publish an MSE fragment ladder plus manifest for the VideoPlayback dashboard component. | Reads the capture input node directly (not hydrated CSV or JSON); single parent; CPU bound (decode dominates); requires retention.skipArchive; output is for the HEAT dashboard only. | video-atlas-encode |
adaptive-video-encode | Catalogue the StreamComponent video streams present in the capture (names, codec, dimensions, chunk counts) and push the catalogue onto a child input node. | Catalogue only: the HLS and DASH ladder it was designed for was never built, so it produces no playable video. Use it to see what video a capture holds before configuring the atlas. | adaptive-video-encode |
Which node
Expected topology
The atlas node must be a direct child of the capture input node that receives the protobuf uploads. It decodes the StreamComponent access units itself. Do not hang it off hydrate-protobuf or any tabular node: those strip the video, and an intermediate “extracted video” node would only duplicate tens of gigabytes.
Three publishing shapes are in use:
-
Standalone dimension: an Arbex script reads the manifest (the atlas node’s current output), republishes it as one
valuechannel, and builds a single-row layout with aVideoPlaybackcolumn. Good for a smoke test and for a dedicated video view. -
Inside an existing dashboard: an Arbex script wraps the manifest under a key (for example
output.videoAtlas) so ajson-mergecan carry it beside the other producers, and the dashboard script publishes thevaluechannel and mounts the column. The manifest must pass through unchanged: each window’sinputHashis the token the player uses to notice a rewritten window. -
Inside a ComposableChart: the
video_playbackslice renders the seat tiles in the chart stack, follows the chart realm clock, and is bounded to that clock (a vignette realm plays only its vignette), pairing with theplaybackscrubber andevents_laneslices. See the ComposableChart slice reference.
The channel id and name must be identical, because the components resolve the channel by both.
From capture to pixels
Each seat is a tile in the atlas. The player draws each tile into its own canvas region, so one hardware decoder serves every seat at once.
Memory and CPU
- Decode is the dominant cost, and it is irreducible: every source frame is a reference frame, so nothing can be skipped. Cost scales linearly with the number of admitted streams. The default admits the left eye only; the right eye is nearly the same view and would double the work.
- Memory is bounded by the atlas, not by sources times seats. Decoding is tick-synchronous: for each output tick the node decodes only what that tick needs from each stream, scales it into its tile, and releases it. On the full 24th August export this took peak memory from about 3.8 GB to about 250 MB for the same output. The atlas is capped at 1920 by 1088.
- Each window decodes independently with a short lead-in (default 1 second) so a window’s bytes depend only on the data inside its gather range. That is what makes the result order-independent, at a cost of roughly one eighth extra decode.
- The runner declares
cpuLimit: 4000mandmemoryLimit: 4096Mi. The encoder is pinned to four threads (see the node page for why), so four cores is the useful ceiling per task. - On the dashboard side the player keeps a bounded number of windows in the MSE buffer and evicts the oldest, so browser memory does not grow with session length.
Stall watchdog
A video job that stops making progress must not hold the runner forever. The video-utils container therefore runs its task loop in a child process and watches it from a small parent process.
- The worker records progress on every poll for work and on each unit of work inside a node (a parent blob scanned, a window rendered, an output published).
- When no progress is recorded for
VIDEO_UTILS_STALL_SECONDS(default900, minimum60), the parent kills the worker and the container exits with code70. Kubernetes restarts the container. - After the restart the stalled task is released and counts one retry. Other sessions’ video tasks are claimed first, so one bad job cannot block them. A job that stalls on every attempt fails once its retries are spent.
- A restart for this reason shows as a rising
RESTARTScount on the pod, with exit code70inkubectl describe pod. The parent logs the last recorded phase and task id before it exits. - Set
VIDEO_UTILS_WATCHDOG=offon the container to run the task loop in a single process, as it did before the watchdog existed.
This applies to the video-utils runner only. Other runners are unchanged.