Authoring API

defineVideoMetadata()

Declares discovery-time metadata separately from the React component. The id defaults to the entry's directory path under videos/ and is validated when the module loads. Pass one only to override the path.

defineVideoMetadata({
  id: "launch", // optional: defaults to the directory path
  title: "Product launch",
  description: "A concise introduction.",
  duration: "12s",
  layout: productLayout,
  schema: launchInput,
  defaultProps: {headline: "Ship the story."},
  tags: ["product", "launch"],
  thumbnailFrame: 45,
});

<Video>

Provides the resolved layout, input, prepared data, asset registry, and timeline context. It adds no duration of its own. Scene children are laid out in order; any other child renders for the whole video.

<Scene>

<Scene id="demo" duration="8s" name="Product demo">
  <ProductDemo />
</Scene>

Each scene receives a local frame clock starting at zero and is mounted only while it is on screen. Durations accept seconds (8), "8s", "500ms", or "45f".

<Clip>

Content placed at a time rather than after the thing before it. A clip names its own start, takes no space in the sequence, and moves nothing when its length changes.

<Clip from="2s" duration="3s">
  <LowerThird name="Ada Lovelace" />
</Clip>

It reads its clock from whatever contains it: a child of <Video> is placed against the whole cut, the same clip inside a <Scene> against that scene. Children see a clock starting at zero either way. Without duration it runs to the end of its container.

<Audio>

Places a sound on the timeline. Cues declared inside a scene are offset by that scene's start.

<Audio src="/audio/bed.m4a" gain={0.75} fadeIn="1s" fadeOut="1.5s" duckUnder="foreground" />

See Audio for placement, ducking, preview, and mixing.

<Fill> and <SafeArea>

Layout primitives. SafeArea insets its children by the inherited layout safe area.

defineVideoLayout() and defineBrand()

Define inherited format and presentation policy. Layouts cannot perform async work or add frames. See Layouts and brands.

defineInputSchema()

A serializable input contract with parse(), safeParse(), defaults(), and describe(). Studio generates controls from the description. Any zod-compatible schema also works.

definePrepare()

Defines the sole asynchronous data boundary. Its result must be serializable and is frozen into the render manifest.

Frame hooks

const frame = useFrame();
const {fps, width, height, durationInFrames} = useVideo();
const scale = useDesignScale();
const brand = useBrand();
const layout = useLayout();
const scene = useScene();
const assets = useAssets();

Motion must derive from the current frame. Wall-clock timers, CSS keyframes, and requestAnimationFrame are not deterministic render primitives.

Motion helpers

interpolate(frame, [0, 20], [0, 1], {easing: Easing.standard});
spring({frame, fps, from: 0.96, to: 1, damping: 20});

interpolate clamps by default, blends matching numeric segments inside strings, and accepts extrapolateLeft and extrapolateRight. spring solves a damped spring from the frame index alone.

Playback

const playback = usePlayback({fps: 30, durationInFrames: 360, autoPlay: true});
playback.toggle();
playback.step(1);
playback.seek(120);

<Viewer> wraps this hook with a canvas and controls. usePlayback is exported so a custom surface, such as Studio, can own its own transport.

Rendering surfaces

ExportPurpose
OdoriRuntimeMount one video at one frame
ViewerEmbeddable, seekable player with optional controls
RenderSurfaceThe render worker target, with a frame setter and readiness handshake
createRenderManifestFreeze inputs, format, scenes, audio, assets, and fonts
useAudioPlaybackDrive cue playback from a frame clock

On this page