Skip to content
Esc
navigateopen⌘Jpreview
On this page

Data and assets

Resolve async work before playback, freeze mutable inputs, and keep frame rendering deterministic.

React frame rendering must be pure: the same frame and props should produce the same pixels. Put network, database, filesystem, and secret-backed work in prepare.ts.

import {definePrepare} from "odori";

export const prepare = definePrepare(async ({input, assets, cache, signal}) => {
  const release = await cache.getOrSet(
    `release:${input.tag}`,
    () => fetchRelease(input.tag, {signal}),
  );

  const logo = await assets.resolve("brand:product-mark");

  return {release, logo};
});

The prepared result becomes a prop of the video entry:

export default function ChangelogVideo({prepared}) {
  return (
    <Video>
      <Scene duration="6s">
        <ReleaseTitle release={prepared.release} logo={prepared.logo} />
      </Scene>
    </Video>
  );
}

Because the result is frozen into the manifest, a video keeps a sensible fallback for the case where preparation has not run:

export default function WorkflowVideo({prepared}: {prepared?: {commands: Step[]}}) {
  return <Terminal steps={prepared?.commands ?? FALLBACK} />;
}

Inputs

All preview and render inputs must be serializable and schema-valid. One contract powers Studio controls, embedded Player props, CLI input, and export jobs.

import {defineInputSchema} from "odori";

export const launchInput = defineInputSchema({
  headline: {type: "text", defaultValue: "Author the story.", maxLength: 64, multiline: true},
});

defineInputSchema validates, fills defaults, and describes itself so Studio can generate controls. Any zod-compatible object with a parse() method is accepted instead.

Readiness and integrity

Fonts and images resolve before the first frame is captured, and audio is collected into a track the encoder mixes. Assets declared in odori.config.ts are addressable by reference through useAssets().

Every font, asset, and audio source in the manifest carries a real content hash. Local files hash their bytes; remote files are fetched once and cached by URL under .odori/cache/integrity.json. A source that cannot be read is recorded as unresolved rather than pretending to be verified.

font Geist Sans /fonts/Geist-Variable.woff2 sha256-o2n89WKOoqpOG54uxqWzYk42W9pYjh8PLxK1ZPco+7g=

Cache keys

Prepared data is cached on disk under .odori/cache/prepare/, keyed by video source hash, validated input, and prepare version. Changing scene styling does not refetch source data; changing a data dependency invalidates deterministically. Repeated stills and exports of an approved cut reuse the cached result, and a retry never reruns preparation at all because it replays the frozen manifest.

Was this page helpful?