Files
hyperframes/docs/packages/engine.mdx
T
Miguel Ángel 395fb9c084 feat: add browser GPU render mode (#571)
## Problem

HyperFrames already had `--gpu`, but that flag only controlled FFmpeg hardware encoding. The browser capture path still forced Chrome/WebGL through SwiftShader software GL via `--use-angle=swiftshader`, so WebGL-heavy local renders could leave the biggest bottleneck on the CPU path.

That made the existing flag naming easy to misread: `--gpu` sounded like it accelerated the whole render, but it did not change the browser frame-capture backend.

## What this fixes

- Enables host browser GPU acceleration automatically for local CLI renders.
- Adds `--no-browser-gpu` as the local opt-out for software Chrome/WebGL capture.
- Keeps `--browser-gpu` as an explicit local browser-GPU request.
- Adds `browserGpuMode: "software" | "hardware"` to engine config, with `PRODUCER_BROWSER_GPU_MODE` env support for lower-level producer users.
- Keeps Docker browser capture on the deterministic software path.
- Maps hardware browser GPU mode to platform-native Chrome backends:
  - macOS: Metal-backed ANGLE
  - Windows: D3D11-backed ANGLE
  - Linux: EGL
- Blocks explicit `--browser-gpu --docker` with a clear error because Docker browser GPU passthrough is not cross-platform.
- Clarifies docs so `--gpu` means FFmpeg encoder GPU and browser GPU means Chrome/WebGL capture GPU.
- Keeps encoder backend selection auto-detected from FFmpeg capabilities:
  - NVIDIA: NVENC
  - macOS: VideoToolbox
  - Linux: VAAPI
  - Intel: QSV

## Why two flags

There are two separate GPU surfaces in the render pipeline:

1. Browser GPU controls Chrome frame capture.
   - Affects WebGL, canvas, CSS rendering, compositing, and screenshot capture inside the browser.
   - This is enabled automatically for local CLI renders.
   - Use `--no-browser-gpu` when you want the software browser baseline.

2. `--gpu` controls FFmpeg video encoding.
   - Affects the final encode step after frames have already been captured.
   - The concrete encoder is auto-detected from the host FFmpeg build and hardware.
   - It can be faster for some machines/codecs, but it is not equivalent to browser rendering acceleration.

The controls stay independent because users may want:

- `hyperframes render` for the fast local default with browser GPU capture.
- `hyperframes render --no-browser-gpu` for the software-browser local baseline.
- `hyperframes render --gpu` for browser GPU capture plus hardware FFmpeg encoding.
- `hyperframes render --no-browser-gpu --gpu` for software browser capture plus hardware FFmpeg encoding.
- `hyperframes render --docker` for deterministic browser capture.

## Why `--gpu` does not imply browser GPU

Keeping `--gpu` scoped to FFmpeg encoding avoids a semantic break and keeps the risk profile explicit:

- `--gpu` already means encoder acceleration. Expanding it to also change Chrome capture would silently alter behavior for users who only wanted hardware encoding.
- Browser GPU and encoder GPU have different portability. Encoder GPU can work in Docker when the host exposes the right devices; browser GPU passthrough is not cross-platform, so this PR intentionally blocks explicit `--browser-gpu --docker`.
- The Apple presentation benchmark shows why the controls should stay separate: browser GPU capture was the useful improvement, while macOS VideoToolbox via `--gpu` was slower and produced larger output for this `standard` H.264 run.

If HyperFrames later wants a single umbrella acceleration control, it should be explicit, for example `--acceleration browser|encoder|all` or `--gpu=browser|encoder|all`, rather than changing the meaning of the existing boolean `--gpu`.

## Root cause

`buildChromeArgs()` always injected `--use-gl=angle --use-angle=swiftshader`. `disableGpu` only appended `--disable-gpu`; it did not provide a hardware-GPU mode. That made the public `--gpu` flag look broader than it was, because render capture stayed software-backed even when encoder GPU was requested.

## Verification

### Local checks

- `bun install`
- `bun run build:hyperframes-runtime`
- `bun run --filter @hyperframes/engine test src/config.test.ts src/services/browserManager.test.ts`
- `bun run --filter @hyperframes/cli test src/utils/dockerRunArgs.test.ts src/commands/render.test.ts`
- `bun run --filter @hyperframes/cli typecheck`
- `bun run --filter @hyperframes/engine typecheck`
- `bun run --filter @hyperframes/producer typecheck`
- `cd packages/producer && bunx vitest run src/services/renderOrchestrator.test.ts`
- `bunx oxlint packages/cli/src/commands/render.ts packages/cli/src/commands/render.test.ts packages/cli/src/utils/dockerRunArgs.ts packages/cli/src/utils/dockerRunArgs.test.ts packages/engine/src/config.ts packages/engine/src/config.test.ts packages/engine/src/services/browserManager.ts packages/engine/src/services/browserManager.test.ts packages/producer/src/services/renderOrchestrator.test.ts`
- `bunx oxfmt --check ...` on changed source/docs files
- `git diff --check`
- `bun packages/cli/src/cli.ts render --help | rg -n "browser-gpu|no-browser-gpu|GPU"`
- `bun packages/cli/src/cli.ts render packages/producer/tests/css-spinner-render-compat/src --output /tmp/hf-auto-browser-gpu-smoke.mp4 --workers 1 --quality draft --fps 24 --strict`
  - Render plan prints `GPU: browser GPU (auto)`.
- `bun packages/cli/src/cli.ts render packages/producer/tests/css-spinner-render-compat/src --no-browser-gpu --output /tmp/hf-software-browser-gpu-smoke.mp4 --workers 1 --quality draft --fps 24 --strict`
  - Render plan does not print browser GPU.
- `bun packages/cli/src/cli.ts render packages/producer/tests/css-spinner-render-compat/src --docker --browser-gpu --output /tmp/should-not-render.mp4`
  - Exits 1 with `Browser GPU is local-only`.
- `buildDockerRunArgs()` regression coverage asserts Docker container args include `--no-browser-gpu`, preventing nested container renders from re-enabling browser GPU through the local CLI default.
- `resolveBrowserGpuForCli()` regression coverage asserts `PRODUCER_BROWSER_GPU_MODE=software` opts out when no CLI browser-GPU flag is supplied, while explicit `--browser-gpu` / `--no-browser-gpu` still win.
- `ffmpeg -v error -i /tmp/hf-auto-browser-gpu-smoke.mp4 -f null -`
- `ffmpeg -v error -i /tmp/hf-software-browser-gpu-smoke.mp4 -f null -`
- `ffprobe -v error -show_entries format=duration:stream=codec_name,width,height,r_frame_rate -of json /tmp/hf-browser-gpu-smoke.mp4` -> H.264, 1920x1080, 24fps, 5.0s

### Apple presentation benchmark

Rendered `/Users/miguel07code/Downloads/apple-presentation.zip` as supplied after extracting to `/tmp/hf-apple-profile/apple-presentation`.

Fixed settings:

- 1920x1080
- 30fps
- `standard` quality
- 4240 frames
- 141.32s duration
- 8-worker cap; render auto-calibration used 6 capture workers
- macOS host detected FFmpeg GPU encoder: `videotoolbox`

| Mode | Equivalent flags after this PR | Wall time | vs software-browser baseline | Speed | Capture | Encode | Output |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
| Software browser + CPU encode | `--no-browser-gpu` | 120.77s | baseline | 1.17x | 97.87s | 10.04s | 8.38MB |
| Browser GPU + CPU encode | default local render | 70.10s | 42.0% faster | 2.02x | 50.72s | 9.91s | 8.39MB |
| Software browser + encoder GPU | `--no-browser-gpu --gpu` | 133.16s | 10.3% slower | 1.06x | 103.58s | 18.31s | 25.43MB |
| Browser GPU + encoder GPU | `--gpu` | 74.12s | 38.6% faster | 1.91x | 46.69s | 17.93s | 25.45MB |

Result: browser GPU capture is the meaningful improvement for this WebGL/browser-capture-heavy presentation. VideoToolbox encoding was slower and produced larger files for this current `standard` H.264 path, so `--gpu` should stay separate and opt-in.

Why `--gpu` plus browser GPU was slower than browser GPU alone: the combined run captured about 4.0s faster than browser GPU alone, but VideoToolbox encoding was about 8.0s slower than CPU x264 encoding, so the encode loss outweighed the capture gain.

### VideoToolbox flag check

I also isolated the encode stage against the already-captured Apple frames to check whether macOS GPU encoding only needed special flags.

`ffmpeg -h encoder=h264_videotoolbox` does not expose a CRF/CQ-style quality option like x264. It exposes bitrate-oriented and VideoToolbox-specific options such as `-b:v`, `-realtime`, `-profile`, `-coder`, `-prio_speed`, `-power_efficient`, and `-allow_sw`. That means our current `-q:v` mapping is not equivalent to x264 CRF and can produce very different bitrate/size behavior.

Measured full-frame encode variants on this host:

| VideoToolbox variant | Encode wall time | Output size | Bitrate |
| --- | ---: | ---: | ---: |
| Current `-q:v 64 -allow_sw 1` | 18.76s | 25.31MB | 1.43 Mbps |
| Current without `-allow_sw 1` | 18.21s | 25.31MB | 1.43 Mbps |
| `-b:v 500k -maxrate 750k -bufsize 1000k -profile high -coder cabac -realtime 1 -prio_speed 1 -power_efficient 0` | 20.58s | 7.42MB | 0.42 Mbps |
| Same with `-b:v 1500k` | 20.84s | 16.70MB | 0.95 Mbps |
| `-b:v 500k -profile baseline -coder cavlc -realtime 1 -prio_speed 1 -power_efficient 0` | 18.11s | 8.94MB | 0.51 Mbps |

Conclusion: VideoToolbox can be made size/bitrate-predictable with explicit `--video-bitrate`, but the tested speed-oriented flags did not make it faster than CPU x264 wall time for this render. That reinforces keeping `--gpu` encoder acceleration explicit and separate from browser GPU capture.

Artifacts from the local benchmark:

- `/tmp/hf-apple-profile/results/cpu.mp4`
- `/tmp/hf-apple-profile/results/browser-gpu.mp4`
- `/tmp/hf-apple-profile/results/encoder-gpu.mp4`
- `/tmp/hf-apple-profile/results/full-gpu.mp4`
- `/tmp/hf-apple-profile/results/summary.json`

All four benchmark MP4s completed `ffprobe` and full `ffmpeg -f null` decode checks.

### Pixel comparison

Compared decoded MP4 output between software-browser and browser-GPU renders:

- Apple presentation:
  - 4240 frames compared
  - 636 exact matching decoded frame hashes
  - 3604 different decoded frame hashes
  - Average PSNR: 57.79 dB
- `css-spinner-render-compat` clean fixture:
  - 120 frames compared
  - 0 exact matching decoded frame hashes
  - Average PSNR: 61.57 dB

Interpretation: browser GPU output is not strict hash/pixel-identical to the software-browser path after lossy H.264 encode, but the measured deltas are visually tiny. Above 50 dB PSNR is typically visually indistinguishable for normal video review. Use `--no-browser-gpu` or Docker when strict cross-run/cross-machine reproducibility matters more than local speed.

### Browser verification

- Started HyperFrames Studio preview for `packages/producer/tests/css-spinner-render-compat/src`.
- Used `agent-browser` to open `http://localhost:5191#project/src` and verify the composition loaded in Studio.
- Screenshots:
  - `/tmp/hf-gpu-browser-proof/preview-loaded.png`
  - `/tmp/hf-gpu-browser-proof/preview-playing.png`
  - `/tmp/hf-gpu-browser-proof/preview-frame-60.png`
- Agent-browser recordings:
  - `/tmp/hf-gpu-browser-proof/preview-playback.webm`
  - `/tmp/hf-gpu-browser-proof/preview-seek.webm`

## Notes

- Browser GPU is enabled automatically for local CLI renders and disabled in Docker.
- `--no-browser-gpu` is the opt-out for software Chrome/WebGL capture.
- `--gpu` remains encoder-only and opt-in.
- The Apple presentation zip has existing lint errors around unmanaged nested videos and imperative media `play()` calls. The benchmark still compares the same supplied source across modes, but it should not be treated as a clean deterministic-composition fixture.
2026-04-30 06:46:14 +02:00

416 lines
14 KiB
Plaintext

---
title: "@hyperframes/engine"
description: "Seekable page-to-video capture engine using Chrome's BeginFrame API."
---
The engine package provides the low-level video capture pipeline: it loads an HTML page in headless Chrome, seeks to each frame independently, and captures pixel buffers using Chrome's `HeadlessExperimental.beginFrame` API. This is the layer that makes Hyperframes rendering deterministic.
```bash
npm install @hyperframes/engine
```
## When to Use
<Warning>
**Most users should NOT use the engine directly.** Use the [CLI](/packages/cli) (`npx hyperframes render`) or the [producer](/packages/producer) package instead — they handle runtime injection, audio mixing, and encoding for you.
</Warning>
**Use `@hyperframes/engine` when you need to:**
- Build a custom rendering pipeline with full control over frame capture
- Integrate Hyperframes capture into an existing video processing system
- Capture individual frames (e.g., for thumbnails or sprite sheets) without encoding to video
- Implement a custom encoding backend (not FFmpeg)
**Use a different package if you want to:**
- Render an HTML composition to a finished MP4 or WebM — use the [producer](/packages/producer) or [CLI](/packages/cli)
- Preview compositions in the browser — use the [CLI](/packages/cli) or [studio](/packages/studio)
- Lint or parse composition HTML — use [core](/packages/core)
## How It Works
The engine implements a **seek-and-capture** loop that is fundamentally different from screen recording:
<Steps>
<Step title="Launch headless Chrome">
The engine starts `chrome-headless-shell`, a minimal headless Chrome binary optimized for programmatic control via the Chrome DevTools Protocol (CDP).
</Step>
<Step title="Load the composition">
Your HTML composition is loaded into a browser page. The Hyperframes runtime is injected to manage timeline seeking.
</Step>
<Step title="Seek to each frame">
For every frame in the video (e.g., 900 frames for a 30-second video at 30fps), the engine calls `renderSeek(time)` to advance the composition to the exact timestamp. No wall clock is involved — each frame is independently positioned.
</Step>
<Step title="Capture via BeginFrame">
Chrome's `HeadlessExperimental.beginFrame` API captures the compositor output as a pixel buffer. This produces pixel-perfect frames without any screen recording artifacts.
</Step>
<Step title="Hand off frames">
Captured frame buffers are passed to a consumer — typically FFmpeg (via the producer) for encoding into MP4, but you can provide your own consumer.
</Step>
</Steps>
This approach guarantees [deterministic rendering](/concepts/determinism): the same HTML always produces the identical video, regardless of system load or timing.
## Configuration
```typescript
import { resolveConfig, DEFAULT_CONFIG } from '@hyperframes/engine';
import type { EngineConfig } from '@hyperframes/engine';
// Use defaults
const config = DEFAULT_CONFIG;
// Or resolve with overrides
const config = resolveConfig({
// ... custom options
});
```
### Quality Presets
| Preset | Use Case | Speed |
|--------|----------|-------|
| `draft` | Fast iteration during development | Fastest |
| `standard` | Production renders with good quality/speed balance | Moderate |
| `high` | Final delivery, maximum quality | Slowest |
### FPS Options
| FPS | Use Case |
|-----|----------|
| `24` | Cinematic look, smaller file size |
| `30` | Standard web video, good balance |
| `60` | Smooth motion, UI animations, screen recordings |
## Programmatic Usage
The engine uses a session-based API for frame capture:
```typescript
import {
createCaptureSession,
initializeSession,
captureFrame,
captureFrameToBuffer,
getCompositionDuration,
closeCaptureSession,
} from '@hyperframes/engine';
// 1. Create a capture session
const session = await createCaptureSession({ fps: 30, width: 1920, height: 1080 });
// 2. Initialize with a composition
await initializeSession(session, './my-video/index.html');
// 3. Get the total duration
const duration = getCompositionDuration(session);
// 4. Capture frames
const totalFrames = Math.ceil(duration * 30);
for (let i = 0; i < totalFrames; i++) {
// Capture to disk
const result = await captureFrame(session, i);
// result.path, result.captureTimeMs
// Or capture to buffer (in-memory)
const bufResult = await captureFrameToBuffer(session, i);
// bufResult.buffer, bufResult.captureTimeMs
}
// 5. Clean up
await closeCaptureSession(session);
```
### Browser Management
```typescript
import {
acquireBrowser,
releaseBrowser,
resolveHeadlessShellPath,
buildChromeArgs,
} from '@hyperframes/engine';
// Acquire a browser instance (creates or reuses from pool)
const browser = await acquireBrowser();
// Get the Chrome binary path
const chromePath = await resolveHeadlessShellPath();
// Release when done
await releaseBrowser(browser);
```
### Encoding
The engine includes FFmpeg encoding utilities with support for MP4 (h264) and WebM (VP9 with alpha):
```typescript
import {
encodeFramesFromDir,
muxVideoWithAudio,
applyFaststart,
detectGpuEncoder,
getEncoderPreset,
ENCODER_PRESETS,
} from '@hyperframes/engine';
// Get format-aware encoder settings
const mp4Preset = getEncoderPreset('standard', 'mp4');
// { codec: "h264", pixelFormat: "yuv420p", preset: "medium", quality: 23 }
const webmPreset = getEncoderPreset('standard', 'webm');
// { codec: "vp9", pixelFormat: "yuva420p", preset: "good", quality: 23 }
// Encode captured frames to video
await encodeFramesFromDir(framesDir, 'frame_%06d.png', outputPath, {
fps: 30,
...webmPreset,
});
// Mix video with audio (uses Opus for WebM, AAC for MP4)
await muxVideoWithAudio(videoPath, audioPath, outputPath);
// Apply MP4 faststart for streaming (no-op for WebM)
await applyFaststart(inputPath, outputPath);
// Detect GPU encoding support
const gpu = await detectGpuEncoder();
// gpu: "nvenc" | "videotoolbox" | "vaapi" | "qsv" | null
```
#### WebM with VP9 Alpha
When encoding for transparency, use `format: "webm"` with `getEncoderPreset()`. This configures:
- **VP9 codec** (`libvpx-vp9`) with alpha-capable `yuva420p` pixel format
- **`-auto-alt-ref 0`** and **`alpha_mode=1`** metadata for proper alpha encoding
- **`-row-mt 1`** for multi-threaded VP9 encoding
- **Opus audio** in the mux step (instead of AAC for MP4)
### Streaming Encoder
For memory-efficient encoding without writing frames to disk:
```typescript
import { spawnStreamingEncoder } from '@hyperframes/engine';
const encoder = await spawnStreamingEncoder({
outputPath: './output.mp4',
fps: 30,
width: 1920,
height: 1080,
});
// Feed frames directly to encoder
encoder.writeFrame(frameBuffer);
// ...
const result = await encoder.finalize();
```
### Video Frame Extraction
Extract frames from source video files for injection into the browser:
```typescript
import {
parseVideoElements,
extractAllVideoFrames,
getFrameAtTime,
createFrameLookupTable,
FrameLookupTable,
} from '@hyperframes/engine';
// Parse video elements from HTML
const videos = parseVideoElements(html);
// Extract all frames from a video
const frames = await extractAllVideoFrames(videoPath, { fps: 30 });
// Create a lookup table for fast frame access
const lookup = createFrameLookupTable(frames);
const frame = lookup.getFrameAtTime(5.0);
```
### Audio Processing
```typescript
import { parseAudioElements, processCompositionAudio } from '@hyperframes/engine';
// Parse audio elements from HTML
const audioElements = parseAudioElements(html);
// Process and mix all audio tracks
const mixResult = await processCompositionAudio({ audioElements, duration, fps });
```
### Parallel Rendering
```typescript
import {
calculateOptimalWorkers,
distributeFrames,
executeParallelCapture,
getSystemResources,
} from '@hyperframes/engine';
// Check system resources
const resources = getSystemResources();
// Calculate optimal worker count
const workers = calculateOptimalWorkers(totalFrames);
// Distribute frames across workers
const tasks = distributeFrames(totalFrames, workers);
// Execute parallel capture
const results = await executeParallelCapture(tasks);
```
### File Server
Serve composition files over HTTP for the browser to load:
```typescript
import { createFileServer } from '@hyperframes/engine';
const server = await createFileServer({ root: './my-video', port: 0 });
// server.url, server.port
// ... use server.url as the composition URL
await server.close();
```
## HDR APIs
The engine exports two layers of HDR support: **color-space utilities** that classify sources and configure the FFmpeg encoder, and a **WebGPU readback runtime** for capturing CSS-animated DOM directly into HDR.
For end-to-end HDR rendering (HDR video and image sources composited into an HDR10 MP4) use the [producer](/packages/producer) or the CLI render pipeline with HDR auto-detect / `--hdr` / `--sdr` — see [HDR Rendering](/guides/hdr). The APIs below are for custom integrations.
### Color space utilities
```typescript
import {
isHdrColorSpace,
detectTransfer,
analyzeCompositionHdr,
getHdrEncoderColorParams,
DEFAULT_HDR10_MASTERING,
} from '@hyperframes/engine';
import type { HdrTransfer, HdrEncoderColorParams, HdrMasteringMetadata } from '@hyperframes/engine';
// Classify a single source from its ffprobe color space
isHdrColorSpace(colorSpace); // boolean — true for BT.2020 / PQ / HLG
detectTransfer(colorSpace); // 'pq' | 'hlg' (gate on isHdrColorSpace first)
// Pick the dominant transfer across many sources
analyzeCompositionHdr([cs1, cs2]); // { hasHdr, dominantTransfer: 'pq' | 'hlg' | null }
// Build the FFmpeg color params + HDR10 static metadata for x265
const params = getHdrEncoderColorParams('pq');
// {
// colorPrimaries: 'bt2020',
// colorTrc: 'smpte2084',
// colorspace: 'bt2020nc',
// pixelFormat: 'yuv420p10le',
// x265ColorParams: 'colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:master-display=...:max-cll=1000,400',
// mastering: { masterDisplay: '...', maxCll: '1000,400' },
// }
```
`getHdrEncoderColorParams` always includes both color tagging *and* the HDR10 static metadata (mastering display + content light level). Without that metadata, downstream players treat the file as SDR BT.2020 and tone-map incorrectly. Pass a custom `HdrMasteringMetadata` if you have measured per-content values; otherwise the conservative `DEFAULT_HDR10_MASTERING` defaults match how most HDR10 grading suites tag content.
### WebGPU HDR DOM capture
For capturing CSS-animated DOM directly into HDR (no FFmpeg source involved), the engine exposes a separate WebGPU pipeline:
```typescript
import {
launchHdrBrowser,
buildHdrChromeArgs,
initHdrReadback,
uploadAndReadbackHdrFrame,
float16ToPqRgb,
} from '@hyperframes/engine';
// Launch headed Chrome with WebGPU enabled
const { browser, page } = await launchHdrBrowser({ width: 1920, height: 1080 });
// Inject the WebGPU readback runtime
const ok = await initHdrReadback(page, 1920, 1080);
// For each frame: upload float16 pixels, read back float16 RGBA
const { rgba16, bytesPerRow } = await uploadAndReadbackHdrFrame(page, float16Base64);
// Convert linear float16 → PQ-encoded 16-bit RGB suitable for piping into ffmpeg/x265
const pqRgb = float16ToPqRgb(rgba16, width, height, bytesPerRow);
```
<Warning>
This path requires **headed Chrome with `--enable-unsafe-webgpu`** — WebGPU is unavailable in `chrome-headless-shell`. It is *not* used by the default HDR-aware render pipeline (which extracts HDR pixels from sources via FFmpeg and composites in Node). Use it only for advanced custom pipelines that need CSS animations driving HDR pixel output.
</Warning>
## The `window.__hf` Protocol
The engine communicates with the browser page via the `window.__hf` protocol. Any page that implements this protocol can be captured by the engine — you are not limited to Hyperframes compositions.
```typescript
// The page must expose this on window.__hf
interface HfProtocol {
duration: number; // Total duration in seconds
seek(time: number): void; // Seek to a specific time
media?: HfMediaElement[]; // Optional media element declarations
}
interface HfMediaElement {
elementId: string; // DOM element ID
src: string; // Media source URL
startTime: number; // Start time on timeline
endTime: number; // End time on timeline
mediaOffset?: number; // Playback offset in source
volume?: number; // Volume (0-1)
hasAudio?: boolean; // Whether element has audio
}
```
## Key Concepts
### BeginFrame Rendering
Traditional screen capture records at wall-clock speed — if your system is under load, frames get dropped. The engine uses Chrome's `HeadlessExperimental.beginFrame` to explicitly advance the compositor, producing each frame on demand. This means:
- **No dropped frames** — every frame is captured
- **No timing dependency** — a 60-second video does not take 60 seconds to capture
- **Pixel-perfect output** — the compositor produces the exact pixels it would display
For more on how this enables deterministic output, see [Deterministic Rendering](/concepts/determinism).
### Seek Contract
The engine relies on the Hyperframes runtime's `renderSeek(time)` function. When called, `renderSeek`:
1. Pauses all GSAP timelines
2. Seeks every timeline to the exact timestamp
3. Updates all media elements (video, audio) to match
4. Mounts/unmounts clips based on their `data-start` and `data-duration`
This contract is what makes frame-by-frame capture possible — each frame is a complete, independent snapshot of the composition at that point in time.
### Chrome Requirements
The engine requires `chrome-headless-shell`, which is included when you install the package. It uses a pinned Chrome version to ensure consistent rendering across environments. For fully deterministic output (including fonts), use Docker mode via the [producer](/packages/producer).
## Related Packages
<CardGroup cols={2}>
<Card title="Producer" icon="film" href="/packages/producer">
Wraps the engine with runtime injection, FFmpeg encoding, and audio mixing for complete MP4 output.
</Card>
<Card title="Core" icon="cube" href="/packages/core">
Provides the types, runtime, and linter that the engine depends on.
</Card>
<Card title="CLI" icon="terminal" href="/packages/cli">
The easiest way to render — calls the producer (and engine) under the hood.
</Card>
<Card title="Studio" icon="palette" href="/packages/studio">
Visual editor for building compositions before rendering them with the engine.
</Card>
</CardGroup>