Video files

Each video.tsx file exports two things:

  1. a static metadata export that Odori can read without rendering frames
  2. a default React component that defines the scenes
videos/launch/video.tsx
import {Scene, Video, defineVideoMetadata} from "odori";
import {BrowserDemo} from "../components/browser-demo/browser-demo";
import {EndCard} from "../components/end-card/end-card";
import {TitleReveal} from "../components/title-reveal/title-reveal";
import {launchInput} from "./schema";

export const metadata = defineVideoMetadata({
  title: "Product launch",
  duration: "24s",
  schema: launchInput,
  defaultProps: {
    headline: "Meet the new workflow.",
    productUrl: "https://example.com",
  },
});

export default function LaunchVideo({headline, productUrl}) {
  return (
    <Video>
      <Scene id="opening" duration="4s">
        <TitleReveal title={headline} />
      </Scene>
      <Scene id="demo" duration="16s">
        <BrowserDemo url={productUrl} />
      </Scene>
      <Scene id="resolution" duration="4s">
        <EndCard title="Available today." />
      </Scene>
    </Video>
  );
}

JSX and metadata

Use JSX to compose components, loops, and conditional content. Export metadata separately so Odori can discover the video's ID, format, inputs, and duration.

Structured scenes and direct React

Use <Scene> for most work. It gives Odori inspectable boundaries, local frame zero, duration checks, scene names in Studio, and better diagnostics.

<Scene id="proof" duration="6s">
  <CodeProof code={source} />
</Scene>

For continuous motion, use Odori frame primitives inside the video:

export default function GenerativeVideo() {
  const frame = useFrame();
  return (
    <Video>
      <GenerativeCanvas progress={frame / 360} />
    </Video>
  );
}

When Odori cannot derive scene duration, metadata.duration is authoritative. The framework validates that structured scene totals agree with it.

Scenes are sequenced, clips are placed

Scenes play in order. Changing one scene's duration moves the scenes after it. Use <Clip> to place content at a specific time without shifting the sequence:

<Video>
  <Scene id="demo" duration="10s">
    <BrowserDemo url="odori.dev" />
  </Scene>

  {/* Two seconds in, over the demo, for three seconds. */}
  <Clip from="2s" duration="3s">
    <Captions lines={["Every frame is a function of one number."]} />
  </Clip>
</Video>

A clip reads its clock from wherever it sits, which is what makes it compose. A direct child of <Video> is placed against the whole cut; the same clip inside a <Scene> is placed against that scene and disappears with it. Either way its children see a clock starting at zero, so a component written for a scene behaves identically inside a clip.

Omit duration to keep a clip visible until its container ends:

<Clip from="1s">
  <Watermark />
</Clip>

Adjacent scenes that should cross rather than cut are still a scene concern: overlap pulls a scene back into the one before it, and useSceneTransition() reports how far into that join a frame is.

Inputs

All preview and render inputs must be serializable and schema-valid. A single contract powers Studio controls, embedded Viewer props, CLI input, API requests, and export jobs.

On this page