---
title: Preview and render lifecycle
description: One prepared manifest drives live preview, deterministic stills, tests, and on-demand encoded exports.
---

Odori separates authoring from encoding:

```text
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:

```ts
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](/docs/guides/audio) 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.

```text
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`.

```bash
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.

:::note
An export URL means a job was accepted, not that rendering succeeded. Clients
should follow job state until it is `ready` or `failed`.
:::
