mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
* docs(guides): add performance guide and preview-stutter troubleshooting Adds a dedicated Performance guide covering preview-vs-render cost model, expensive CSS patterns (backdrop-filter, filter, shadows), image sizing, and how to diagnose slow compositions with Chrome DevTools. Cross-links from troubleshooting (new "Preview stutters" accordion) and common-mistakes (new "Oversized source images" and "Heavy backdrop-filter stacks" accordions). Wires the new page into docs.json nav. Also fixes a pre-commit format hook edge case: oxfmt would exit 2 when the only staged files matching the format glob were all covered by .prettierignore (e.g. docs-only changes). Add --no-error-on-unmatched-pattern to the lefthook oxfmt invocation so docs-only commits are not blocked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: call out preview performance limits at the entry points The preview command, studio package, and determinism concept pages all frame preview as visually equivalent to render — correct for fidelity, misleading for playback smoothness. A user who reads those pages and then hits a paint-heavy composition has no way to know why preview stutters, short of drilling into troubleshooting. Adds short notes at each entry point linking out to the new Performance guide, so users hit the "preview is hardware-bound, render isn't" explanation wherever they land first. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
104 lines
5.0 KiB
Plaintext
104 lines
5.0 KiB
Plaintext
---
|
|
title: Deterministic Rendering
|
|
description: "Same input, identical output. Every time."
|
|
---
|
|
|
|
Hyperframes is built around a core guarantee: **the same [composition](/concepts/compositions) always produces the same video**. This is what makes automated pipelines, CI testing, and AI-driven workflows reliable.
|
|
|
|
## How It Works
|
|
|
|
The rendering pipeline is frame-by-frame and seek-driven. No realtime playback is involved -- every frame is independently seeked and captured.
|
|
|
|
<Steps>
|
|
<Step title="Frame clock">
|
|
The [engine](/packages/engine) computes the time for each frame using integer math: `time = floor(frame) / fps`. There is no wall-clock dependency -- rendering is entirely decoupled from real time.
|
|
</Step>
|
|
<Step title="Seek">
|
|
The [frame adapter](/concepts/frame-adapters) receives a `seekFrame(frame)` call and deterministically positions all animations, DOM state, and canvas content to the exact frame. The adapter's `renderSeek` pauses all [GSAP](/guides/gsap-animation) timelines and seeks them to the computed time.
|
|
</Step>
|
|
<Step title="Capture">
|
|
Chrome's `HeadlessExperimental.beginFrame` API captures the pixel buffer for the current frame. This is a single, atomic operation -- no partial paints or race conditions.
|
|
</Step>
|
|
<Step title="Encode">
|
|
FFmpeg encodes the captured frames into the final MP4 video. Audio tracks from `<audio>` and `<video>` elements are mixed in during this stage.
|
|
</Step>
|
|
</Steps>
|
|
|
|
```mermaid
|
|
graph LR
|
|
A["Frame Clock<br/>t = frame / fps"] --> B["Seek<br/>adapter.seekFrame(frame)"]
|
|
B --> C["Capture<br/>beginFrame API"]
|
|
C --> D["Encode<br/>FFmpeg"]
|
|
D --> E["MP4"]
|
|
style A fill:#00C4FF,color:#fff
|
|
style B fill:#00C4FF,color:#fff
|
|
style C fill:#00C4FF,color:#fff
|
|
style D fill:#00C4FF,color:#fff
|
|
style E fill:#00A8E1,color:#fff
|
|
```
|
|
|
|
## What Makes It Deterministic
|
|
|
|
- **No wall-clock dependencies** -- rendering does not use `Date.now()`, `requestAnimationFrame`, or system timers
|
|
- **No unseeded randomness** -- `Math.random()` without a seed breaks determinism
|
|
- **No render-time network fetches** -- all assets must be loaded before rendering starts
|
|
- **Fixed output parameters** -- `fps`, `width`, and `height` are locked before the first frame
|
|
- **Finite duration** -- every [composition](/concepts/compositions) has a known, finite length
|
|
|
|
These same rules apply to every [frame adapter](/concepts/frame-adapters). If you are building a custom adapter, you must follow the [determinism contract](/concepts/frame-adapters#determinism-contract).
|
|
|
|
## Docker Mode
|
|
|
|
For maximum reproducibility, render in Docker:
|
|
|
|
```bash
|
|
npx hyperframes render --docker --output output.mp4
|
|
```
|
|
|
|
Docker mode uses an exact Chrome version and font set, ensuring:
|
|
- Same Chromium rendering engine across all platforms
|
|
- Same system fonts (no platform-specific font substitution)
|
|
- Same FFmpeg encoder version
|
|
|
|
See the [Rendering guide](/guides/rendering) for all rendering options.
|
|
|
|
## Preview vs. Render Parity
|
|
|
|
The browser preview and the rendered MP4 should match. Hyperframes achieves this through:
|
|
|
|
- **One runtime** -- the same `hyperframe.runtime` drives both preview and render
|
|
- **Producer-canonical behavior** -- the [producer's](/packages/producer) seek semantics are the source of truth
|
|
- **Readiness gates** -- `__playerReady` and `__renderReady` ensure the [composition](/concepts/compositions) is fully loaded before any frame is captured
|
|
|
|
Parity here means **visual fidelity** — every frame looks the same. It does *not* mean performance parity. Preview plays in real time in a browser, so frame-rate limits are bound by your hardware. Render is seek-driven and frame-at-a-time, so it never drops frames regardless of per-frame cost. A composition can stutter in preview and render perfectly. See [Performance](/guides/performance) for why.
|
|
|
|
<Note>
|
|
Local rendering (without Docker) may show slight differences due to platform-specific font rendering and Chrome version. Use Docker mode when exact reproducibility matters.
|
|
</Note>
|
|
|
|
## For Adapter Authors
|
|
|
|
If you are building a [frame adapter](/concepts/frame-adapters), your adapter must follow the determinism contract:
|
|
|
|
- `seekFrame(frame)` must be idempotent -- same frame, same result
|
|
- No side effects that depend on call order (must handle random access)
|
|
- No async operations that resolve after the frame is "committed"
|
|
- Clean lifecycle: `init` -> `seekFrame` (N times) -> `destroy`
|
|
|
|
## Next Steps
|
|
|
|
<CardGroup cols={2}>
|
|
<Card title="Frame Adapters" icon="plug" href="/concepts/frame-adapters">
|
|
Build adapters that uphold the determinism contract
|
|
</Card>
|
|
<Card title="Rendering" icon="film" href="/guides/rendering">
|
|
Render to MP4 locally or in Docker
|
|
</Card>
|
|
<Card title="@hyperframes/producer" icon="clapperboard" href="/packages/producer">
|
|
The full rendering pipeline that orchestrates deterministic output
|
|
</Card>
|
|
<Card title="Common Mistakes" icon="triangle-exclamation" href="/guides/common-mistakes">
|
|
Pitfalls that break determinism and how to avoid them
|
|
</Card>
|
|
</CardGroup>
|