Preview and render lifecycle
One prepared manifest drives live preview, deterministic stills, tests, and on-demand encoded exports.
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 storage
Live preview
odori dev mounts the video component in Odori Studio or an embedded Odori
Player. The player owns a seekable frame clock and passes deterministic local
frame state to React. No screenshot sequence, headless browser job, encoder, or
MP4 upload is needed for preview.
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;
};
odori inspect <id> --json prints the manifest for any video. The same inputs
always hash to the same manifest, so network drift, mutable URLs, and database
changes cannot silently produce a different MP4 from the approved cut.
Render jobs
An export records the video ID, manifest hash, output path, progress, attempts,
logs, and result under .odori/builds/. Jobs run through a single-lane queue,
so an export started from Studio and one started from the CLI take exactly the
same path and never fight for the machine. Every write to a job record is
serialized and lands atomically, so progress updates cannot corrupt it.
Odori’s render worker opens the same compiled video in render mode, seeks one frame at a time through a readiness handshake, captures those frames, then mixes any audio cues and encodes with FFmpeg. Frames are captured by several browser workers in parallel; each shard walks an interleaved slice of the timeline, so the work spreads evenly.
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.mp4
Tune 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
odori export --retry job-88e09129a0-msw17lz9
Studio 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.