VideoAnnotator

A video player with an editable, contiguous segment timeline — the labeling surface for splitting a clip into consecutive phases and captioning each one. It owns the <video> element, a transport bar (frame-step, boundary jump, play/pause, playback speed) and a percentage-positioned timeline strip with drag-to-move boundaries and click-to-scrub. Segments always tile the whole clip with no gaps or overlaps (end === next.start); any structural edit resets the verified flag on the affected segments.

The component is controlled on segments + selectedIndex: it runs the split/merge/boundary invariants internally and hands the host a fresh array via onSegmentsChange, so the host owns state and persistence.

Demo

Drag a boundary to move it, double-click a boundary to merge, click a segment to select it, drag anywhere on the track to scrub. Keyboard: Space play/pause, / step frame (Shift = 1s, Alt = jump boundary), ,/. prev/next boundary, s split, Backspace merge, j/k prev/next phase, a approve.

pour liquidvideos/pour_liquid.mp4
0:00.00 / 0:10.00 · f0
1 · Reaches for and grasps the wine bottle on the table
2 · Tilts bottle over red mug, pouring liquid into it
3 · Rights the bottle and lifts it away from the filled mug
0:00
0:01
0:02
0:03
0:04
0:05
0:06
0:07
0:08
0:09
0:10
10 wordsphase 1 · 0:00.00–0:03.50 · frames 1–19

Driving it from a ref

Set enableKeyboard={false} and drive the widget through its imperative handle (split, merge, stepFrame, gotoBoundary, play, pause, toggleApprove) when the host owns its own transport chrome or a global shortcut scheme.

0:00.00 / 0:10.00 · f0
1 · Approach
2 · Pour
0:00
0:01
0:02
0:03
0:04
0:05
0:06
0:07
0:08
0:09
0:10

Props

PropTypeDefaultDescription
videoUrlstring | nullVideo source URL. Empty shows a placeholder in the stage.
videoTitlestringTitle shown in a header row above the video (e.g. the clip name).
videoSubtitlestringMonospace subtitle beside the title (e.g. the file path).
headerLeadingReactNodeElement at the start of the header row (e.g. a "show list" button).
showDescriptionbooleanfalseRender the boxed description editor for the selected segment below the timeline.
onDescriptionChange(index: number, value: string) => voidFired when the description editor changes. Required for it to be interactive.
segmentsSegment[]Controlled segment list. The component enforces the contiguous invariant.
selectedIndexnumberControlled index of the active segment.
onSegmentsChange(next: Segment[]) => voidFired after any structural edit (split / merge / boundary drag).
onSelectedChange(index: number) => voidFired when the active segment changes.
onApproveToggle(index: number, verified: boolean) => voidFired when the user toggles verification (a).
durationnumberAuthoritative clip duration (s). Falls back to the video's loadedmetadata duration, then the largest segment end.
extractFpsnumber | null30Frames-per-second for frame-stepping granularity (/).
srcFpsnumber | null30Source fps for the "· fN" frame readout.
loopbooleantrueLoop playback within the selected segment.
speedsnumber[][0.25,0.5,1,1.5,2]Playback-speed options for the speed dropdown.
enableKeyboardbooleantrueInstall the document-level keyboard shortcuts. Set false to drive only via the ref.
classNamestringExtra classes on the root element.

Segment

FieldTypeDescription
startnumberSegment start time in seconds.
endnumberSegment end time in seconds (= next.start after normalization).
descriptionstringFree-text caption for the phase.
verifiedbooleanHuman-confirmed flag; reset by structural edits.