Authoring API
The public React and TypeScript surface for Odori videos, layouts, inputs, preparation, and assets.
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".
<Stagger>
An offset window inside a scene, for staggered layers.
<Stagger from="1s" duration="2s">
<Caption text="Preview needs no encode." />
</Stagger>
<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 />
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);
<Player> 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
| Export | Purpose |
|---|---|
OdoriRuntime |
Mount one video at one frame |
Player |
Seekable player with optional controls |
RenderSurface |
The render worker target, with a frame setter and readiness handshake |
createRenderManifest |
Freeze inputs, format, scenes, audio, assets, and fonts |
useAudioPlayback |
Drive cue playback from a frame clock |