Preview and render lifecycle
Odori separates authoring from encoding:
Discover source
↓
Resolve layout, props, data, fonts, and assets
↓
Freeze a render manifest
↓
Live React preview ── stills and visual tests
↓
Explicit export request
↓
Render worker → encoded media → durable storageLive preview
Run odori dev to preview videos in Studio. The player advances the frame clock and renders React directly, without encoding a video file.
Prepared manifest
Before rendering, Odori resolves asynchronous inputs into a serializable, content-addressed manifest:
type RenderManifest = {
videoId: string;
sourceHash: string;
manifestHash: string;
input: unknown;
prepared: unknown;
format: {width: number; height: number; fps: number; duration: number; durationInFrames: number};
scenes: Array<{id: string; start: number; durationInFrames: number}>;
audio: Array<{src: string; fromFrame: number; durationInFrames: number; gain: number; integrity: string}>;
assets: Array<{url: string; integrity: string}>;
fonts: Array<{family: string; url: string; integrity: string}>;
createdAt: string;
};Run odori inspect <id> --json to inspect resolved inputs, scene timing, and asset hashes.
Render jobs
Exports record their status, inputs, output path, and logs under .odori/builds/. Studio and the CLI submit jobs to the same queue.
Browser workers capture frames in parallel. FFmpeg mixes the audio cues and encodes the output. An export reports its progress:
odori export launch
job job-88e09129a0 manifest 88e09129a05765b874e102653aba07ed
2 audio cue(s) at 360 frames
captured 360 frames in 7.5s on 4 workers, encoded in 1.1s
ok Exported launch to out/launch.mp4Tune the workers with --concurrency, and the encoder with --preset.
Retries
A failed job keeps its frozen manifest, so a retry re-renders the approved cut without re-resolving inputs or rerunning prepare.ts.
odori jobs list
odori jobs retry job-88e09129a0-msw17lz9Studio shows the same history and offers a retry button on a failed job.
Checks before export
odori test mounts every discovered video, reads the compiled timeline, and samples representative frames. It fails when a declared duration disagrees with the compiled scene total, when a frame is blank, when content escapes the canvas, or when text is too small to read at 1080p.
:::note
An export URL means a job was accepted, not that rendering succeeded. Clients should follow job state until it is ready or failed.
:::