Media overlays
EpisodeVideoStack and EpisodeFrameStack accept per-source overlays — annotation layers drawn over the media: bounding boxes for detections, keypoint skeletons for poses and hands. Attach them to the source; the tile keeps them aligned with the pixels at any player size.
A stack-level showOverlays prop (default true) is the master switch;
each overlay also has its own visible flag and opacity.
Hand skeletons + captions on a video
Two layers on one video, each converted with one call: real 21-joint hand
detections (handOverlayFromDetections — reads pixel space, fps, and
skeleton from the file, applies the per-finger HAND21_STYLE) and
labelled subtask segments rendered as subtitle-style captions
(tracksOverlayFromSubtasks). Both stay glued to the frame during
playback and while scrubbing:
Bounding boxes on a frame stack
Hand-authored synthetic detections in normalized coordinates, keyed by
frame index. The cup only exists on frames 30–75 — sparse keys draw
nothing, so boxes appear and disappear with the data instead of going
stale:
Declaring coordinates
Overlay coordinates live in the media's space — never the player's.
Each overlay says which flavor via space:
- Pixel coordinates carry the frame size they were produced against
(not the runtime
videoWidth, which can differ after transcode/rotation). - Out-of-frame points are legal — partially visible detections draw and get clipped, never clamped.
- Stroke widths and dot radii are CSS px at render size, so a 2px bone reads as 2px on any player.
What draws at time t
Each overlay's data field says how to look up "the items for right now".
Missing data draws nothing — stale items are never carried over.
by: 'frame' | by: 'time' | |
|---|---|---|
| Video stack | key = round(time × fps); fps required (source fps is often fractional). | nearest entry within tolerance (default 0.1 s). |
| Frame stack | key = the displayed image's frameIndex ?? array index. | matched against FrameImage.timestamp. |
On a video, the lookup follows the frame the <video> actually presents
(overlaySync: 'media', the default), so drawings never lead or lag the
pixels; 'cursor' follows the time prop instead (deterministic — SSR,
tests).
Reference
bbox
BBoxStyle | Default | |
|---|---|---|
strokeWidth | 1.5 | |
stroke / fill | palette / transparent | |
showLabel / showScore | true / false | showScore appends: cup 0.92. |
colorBy | 'label' | or 'trackId' / 'fixed'. |
palette | built-in | cycled per colorBy key. |
keypoints
KeypointsStyle | Default | |
|---|---|---|
jointRadius / boneWidth | 2.5 / 1.5 | |
boneColors | — | per-bone, same order as skeleton — how per-finger coloring works. |
jointColors | derived | each joint takes the color of the bone ending at it. |
rootColor | #ffffff | joints no bone ends at (the wrist). |
groupColors | — | whole-instance color per group — the simple mode. |
showBox | false | also draw the detection box. |
minPointScore | 0 | hide low-confidence joints. |
tracks — subtitle-style captions
Labelled time ranges rendered like video subtitles: whichever block spans
the current time draws as an outlined caption at the bottom of the tile.
The data shape is the same TimelineTrack / TrackBlock that
EpisodeTimeline takes, so one dataset feeds
both the caption overlay and a timeline track row:
Time-range based — no coordinate space, no frame keys.
Data formats
Each converter — and the matching schema format: value — expects
exactly the JSON shape below. Data in any other shape needs
format: raw (a file that already contains MediaOverlay JSON) or a
custom conversion in code.
handJoints → keypoints overlay
HandJointsFile — the hand-detection pipeline output (this is the format
of the live example's claru_ego__Ceramics.json). Frames are keyed by
0-based index of the original video; coordinates are full-resolution
pixels in upright orientation. Field-by-field, the conversion is:
subtasks → caption overlay + timeline track
SubtasksFile — subtask segment annotations: one labelled range per
segment, in seconds from video start:
Two converters read it — the same blocks drive the subtitle overlay
and an EpisodeTimeline track:
See both live in the
schema example:
a videoStack panel names the files by field ref with
format: handJoints / format: subtasks, and a timeline panel shows
the same subtasks as a track row.
