mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
## Summary This PR ended up covering the full HDR Docker/docs follow-through plus the producer/engine work needed to make HDR still images render and regress correctly in CI. The branch now does four things: - forwards `--hdr` through the Docker render path in the CLI - adds and expands HDR documentation across the docs site - adds first-class HDR still-image support to the engine/producer pipeline - adds targeted HDR regression coverage, including a CI-safe fallback for PNG HDR metadata detection when `ffprobe` does not expose PNG color tags ## What changed ### CLI and docs - `hyperframes render --docker --hdr` now preserves `--hdr` when invoking the in-container CLI - added a dedicated HDR guide and linked it from CLI, producer, engine, rendering, and common-mistakes docs - documented HDR constraints and verification flow: HDR source requirements, MP4/H.265 Main10 output, PQ/HLG handling, Docker usage, and common SDR fallback causes ### Engine and producer HDR image support - added `ImageElement` support to the engine composition model and parsing path - threaded image elements through producer compilation and orchestration - probed image sources for HDR color spaces so image-only compositions can trigger HDR output without requiring an HDR video source - included HDR image start times in stacking queries so the layered compositor can place images correctly in z-order - integrated HDR image compositing into the layered HDR render loop alongside native HDR video layers and SDR DOM overlays - forced screenshot mode for HDR layered compositing where required to keep DOM/HDR layer composition deterministic - skipped readiness waiting for natively extracted HDR videos in the engine path where it was unnecessary and could block layered HDR flows ### HDR metadata robustness - added a fallback in `extractVideoMetadata()` to read PNG `cICP` metadata directly when `ffprobe` omits color-space fields for PNGs - this specifically fixes CI/Docker detection for the `hdr-image-only` fixture, where the render was falling back to SDR because the PNG was not being recognized as BT.2020 PQ ### Regression coverage and fixture cleanup - added `hdr-image-only`, a regression fixture that validates HDR still-image rendering end to end - added `hdr-pq`, a focused HDR PQ regression fixture for the video path - updated regression CI to run an `hdr` shard with `--sequential hdr-pq hdr-image-only` - removed the older larger `hdr-regression/*` fixture set in favor of the smaller targeted regressions used by CI - added the necessary fixture generation/readme material and checked-in golden outputs for the new HDR tests ## Why The original PR description only covered the CLI flag forwarding and docs work. Since then, the branch also picked up the missing runtime support needed for HDR still images and the regression coverage to keep that path from breaking. The practical issue this closes is: - local host runs could pass while CI failed `hdr-image-only` - the failure was a full-frame visual mismatch caused by SDR fallback, not unstable rendering - root cause was PNG HDR metadata not being surfaced by `ffprobe` in the CI Docker environment - parsing the PNG `cICP` chunk directly makes HDR detection deterministic across environments ## Test plan ### Local targeted checks ```bash bunx oxlint packages/engine/src/utils/ffprobe.ts packages/engine/src/utils/ffprobe.test.ts bunx oxfmt packages/engine/src/utils/ffprobe.ts packages/engine/src/utils/ffprobe.test.ts bun --cwd packages/engine test src/utils/ffprobe.test.ts src/utils/hdr.test.ts ``` ### Producer regression runs on host ```bash bun run --cwd packages/core build:hyperframes-runtime:modular bun --cwd packages/producer test -- --sequential --exclude-tags slow,render-compat,hdr bun --cwd packages/producer test -- --sequential hdr-pq hdr-image-only ``` Observed result: - `fast` shard: 7 passed, 0 failed - `hdr` shard: 2 passed, 0 failed ### CI-equivalent Docker verification ```bash docker build -f Dockerfile.test -t hyperframes-producer:test . docker run --rm \ --security-opt seccomp=unconfined \ --shm-size=4g \ -v "$PWD/packages/producer/tests:/app/packages/producer/tests" \ hyperframes-producer:test \ --sequential hdr-pq hdr-image-only ``` Observed result: - `hdr-image-only`: passed - `hdr-pq`: passed - shard summary: 2 passed, 0 failed ### Specific regression fixed Before the PNG `cICP` fallback, the Docker/CI run failed `hdr-image-only` with: - missing `"[Render] HDR source detected — output: PQ ..."` log line - full-frame visual mismatch across all 100 checkpoints - PSNR ~17 on every frame, indicating a consistent SDR-vs-HDR pipeline mismatch After the fallback, the same Docker path recognizes the PNG as HDR and the shard passes.
320 lines
11 KiB
Plaintext
320 lines
11 KiB
Plaintext
---
|
|
title: "@hyperframes/producer"
|
|
description: "Full HTML-to-video rendering pipeline with encoding, audio mixing, and Docker support."
|
|
---
|
|
|
|
The producer package combines the [engine's](/packages/engine) frame capture with FFmpeg encoding to deliver a complete HTML-to-video rendering pipeline. It supports MP4 (h264) and WebM (VP9 with alpha transparency), and handles runtime injection, readiness gates, audio mixing, and optional Docker-based deterministic rendering.
|
|
|
|
```bash
|
|
npm install @hyperframes/producer
|
|
```
|
|
|
|
## When to Use
|
|
|
|
**Use `@hyperframes/producer` when you need to:**
|
|
- Render compositions to MP4 or WebM programmatically from Node.js (e.g., in a backend service or CI pipeline)
|
|
- Build a custom rendering service with fine-grained control over the pipeline
|
|
- Run visual regression tests against golden baselines
|
|
- Benchmark render performance across different configurations
|
|
|
|
**Use a different package if you want to:**
|
|
- Render from the command line without writing code — use the [CLI](/packages/cli) (`npx hyperframes render`)
|
|
- Preview compositions in the browser — use the [CLI](/packages/cli) or [studio](/packages/studio)
|
|
- Capture frames without encoding — use the [engine](/packages/engine)
|
|
- Lint or parse composition HTML — use [core](/packages/core)
|
|
|
|
<Tip>
|
|
If you are building a web application or script that just needs to render a video, the [CLI](/packages/cli) is the fastest path. The producer package is for when you need programmatic control inside Node.js.
|
|
</Tip>
|
|
|
|
## What It Does
|
|
|
|
The producer orchestrates the full render pipeline:
|
|
|
|
<Steps>
|
|
<Step title="Load the composition HTML">
|
|
Reads your `index.html` and any referenced sub-compositions.
|
|
</Step>
|
|
<Step title="Inject the Hyperframes runtime">
|
|
Adds the runtime script that manages timeline seeking, clip lifecycle, and media playback.
|
|
</Step>
|
|
<Step title="Wait for readiness gates">
|
|
Polls for `window.__playerReady` and `window.__renderReady` to ensure all assets (fonts, images, video) are loaded before capture begins.
|
|
</Step>
|
|
<Step title="Capture frames via the engine">
|
|
Uses the [engine's](/packages/engine) BeginFrame pipeline to capture each frame as a pixel buffer.
|
|
</Step>
|
|
<Step title="Encode to MP4 or WebM via FFmpeg">
|
|
Pipes frame buffers into FFmpeg with the selected quality preset. MP4 uses h264; WebM uses VP9 with alpha transparency support.
|
|
</Step>
|
|
<Step title="Mix audio tracks">
|
|
Extracts audio from video clips and audio elements, applies `data-volume` and `data-media-start` offsets, and mixes them into the final MP4.
|
|
</Step>
|
|
</Steps>
|
|
|
|
## Programmatic Usage
|
|
|
|
The producer uses a two-step API: create a render job configuration, then execute it.
|
|
|
|
```typescript
|
|
import { createRenderJob, executeRenderJob } from '@hyperframes/producer';
|
|
|
|
const job = createRenderJob({
|
|
input: './my-video/index.html',
|
|
output: './output.mp4',
|
|
fps: 30,
|
|
quality: 'standard',
|
|
});
|
|
|
|
const result = await executeRenderJob(job);
|
|
```
|
|
|
|
### Render Configuration
|
|
|
|
```typescript
|
|
import type { RenderConfig } from '@hyperframes/producer';
|
|
|
|
const config: RenderConfig = {
|
|
fps: 30, // 24, 30, or 60
|
|
quality: 'standard', // 'draft', 'standard', or 'high'
|
|
format: 'mp4', // 'mp4' or 'webm' (WebM renders with transparency)
|
|
workers: 4, // Parallel render workers (1-8)
|
|
useGpu: false, // GPU-accelerated encoding
|
|
debug: false, // Debug logging
|
|
};
|
|
```
|
|
|
|
#### WebM with Transparency
|
|
|
|
Set `format: 'webm'` to render with a transparent background using VP9 alpha:
|
|
|
|
```typescript
|
|
const job = createRenderJob({
|
|
fps: 30,
|
|
quality: 'standard',
|
|
format: 'webm',
|
|
});
|
|
|
|
await executeRenderJob(job, './my-overlay', './overlay.webm');
|
|
```
|
|
|
|
When `format: 'webm'`:
|
|
- Frames are captured as PNG (preserves alpha channel)
|
|
- Chrome's page background is set to transparent via CDP
|
|
- FFmpeg encodes with VP9 + `yuva420p` pixel format
|
|
- Audio is encoded as Opus (instead of AAC for MP4)
|
|
|
|
#### HDR Output
|
|
|
|
Set `hdr: true` to enable HDR detection. The producer probes every video and image source for BT.2020 / PQ / HLG color tagging — if any HDR source is found, the output uses H.265 10-bit BT.2020 with HDR10 static metadata. SDR-only compositions are unaffected.
|
|
|
|
```typescript
|
|
const job = createRenderJob({
|
|
fps: 30,
|
|
quality: 'standard',
|
|
format: 'mp4',
|
|
hdr: true,
|
|
});
|
|
|
|
await executeRenderJob(job, './my-video', './output.mp4');
|
|
```
|
|
|
|
When `hdr: true`:
|
|
- Sources are probed via `ffprobe`; PQ takes precedence over HLG when both are present
|
|
- HDR videos and images are extracted as 16-bit linear-light pixels and composited natively
|
|
- SDR DOM overlays are converted from sRGB → BT.2020 before being layered on top
|
|
- Output uses `libx265` with `yuv420p10le` and HDR10 mastering / content-light-level metadata
|
|
- `format` must be `'mp4'` — `'mov'` and `'webm'` fall back to SDR
|
|
- HDR `<img>` support is **still images only**; animated HDR-tagged images use only the first frame
|
|
|
|
For full details on source requirements, fallback rules, and verification, see [HDR Rendering](/guides/hdr).
|
|
|
|
### Progress Callbacks
|
|
|
|
```typescript
|
|
import type { ProgressCallback, RenderStatus } from '@hyperframes/producer';
|
|
|
|
const onProgress: ProgressCallback = (status: RenderStatus) => {
|
|
console.log(`Status: ${status}`);
|
|
// Statuses: "queued" | "preprocessing" | "rendering" | "encoding"
|
|
// | "assembling" | "complete" | "failed" | "cancelled"
|
|
};
|
|
```
|
|
|
|
### Cancellation
|
|
|
|
```typescript
|
|
import { RenderCancelledError } from '@hyperframes/producer';
|
|
|
|
try {
|
|
await executeRenderJob(job);
|
|
} catch (err) {
|
|
if (err instanceof RenderCancelledError) {
|
|
console.log(`Cancelled: ${err.reason}`);
|
|
// reason: "user_cancelled" | "timeout" | "aborted"
|
|
}
|
|
}
|
|
```
|
|
|
|
## HTTP Server
|
|
|
|
The producer includes a built-in HTTP server for running as a rendering service:
|
|
|
|
```typescript
|
|
import { startServer } from '@hyperframes/producer/server';
|
|
|
|
await startServer({ port: 8080 });
|
|
```
|
|
|
|
### Server Endpoints
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| `POST` | `/render` | Blocking render — returns JSON result |
|
|
| `POST` | `/render/stream` | Streaming render with Server-Sent Events |
|
|
| `POST` | `/lint` | Lint a composition for issues |
|
|
| `GET` | `/health` | Health check |
|
|
| `GET` | `/outputs/:token` | Download a rendered MP4 |
|
|
|
|
For custom server integration, use the lower-level handlers:
|
|
|
|
```typescript
|
|
import { createRenderHandlers, createProducerApp } from '@hyperframes/producer/server';
|
|
|
|
// Get individual request handlers
|
|
const handlers = createRenderHandlers(options);
|
|
|
|
// Or get a full Hono app
|
|
const app = createProducerApp(options);
|
|
```
|
|
|
|
## Docker Rendering
|
|
|
|
For deterministic output, the producer can render inside a Docker container with a pinned Chrome version and font set. This guarantees identical output across machines — critical for CI pipelines and production services.
|
|
|
|
```bash
|
|
# Via the CLI (recommended)
|
|
npx hyperframes render --docker --output output.mp4
|
|
```
|
|
|
|
<Info>
|
|
Docker mode requires Docker to be installed and running. Run `npx hyperframes doctor` to verify your environment. See [Deterministic Rendering](/concepts/determinism) for details on what makes Docker mode deterministic.
|
|
</Info>
|
|
|
|
## Quality Presets
|
|
|
|
| Preset | Resolution | Encoding | Use Case |
|
|
|--------|-----------|----------|----------|
|
|
| `draft` | Original | Fast CRF | Quick iteration, previewing edits |
|
|
| `standard` | Original | Balanced CRF | Production renders, sharing |
|
|
| `high` | Original | High-quality CRF | Final delivery, archival |
|
|
|
|
## GPU Encoding
|
|
|
|
The producer supports hardware-accelerated encoding for faster renders:
|
|
|
|
| Platform | Encoder | Flag |
|
|
|----------|---------|------|
|
|
| NVIDIA | NVENC | Auto-detected |
|
|
| macOS | VideoToolbox | Auto-detected |
|
|
| Linux | VAAPI | Auto-detected |
|
|
|
|
GPU encoding is automatically used when available. To check your system's capabilities:
|
|
|
|
```bash
|
|
npx hyperframes doctor
|
|
```
|
|
|
|
## Additional Exports
|
|
|
|
The producer also re-exports key engine functionality for convenience:
|
|
|
|
| Export | Description |
|
|
|--------|-------------|
|
|
| `createCaptureSession()` | Create a frame capture session |
|
|
| `initializeSession()` | Initialize session with a composition |
|
|
| `captureFrame()` / `captureFrameToBuffer()` | Capture individual frames |
|
|
| `closeCaptureSession()` | Clean up a capture session |
|
|
| `getCompositionDuration()` | Get total composition duration |
|
|
| `getCapturePerfSummary()` | Get capture performance metrics |
|
|
| `createFileServer()` | Create an HTTP file server for serving assets |
|
|
| `createVideoFrameInjector()` | Create a video frame injector for page |
|
|
| `resolveConfig()` / `DEFAULT_CONFIG` | Producer configuration |
|
|
| `createConsoleLogger()` / `defaultLogger` | Logging utilities |
|
|
| `quantizeTimeToFrame()` | Convert time to frame boundary |
|
|
| `resolveRenderPaths()` | Resolve render directory paths |
|
|
| `prepareHyperframeLintBody()` / `runHyperframeLint()` | Linting utilities |
|
|
|
|
## Regression Testing
|
|
|
|
The producer includes a regression harness for comparing render output against golden baselines. This is useful for catching visual regressions when changing the runtime, engine, or rendering pipeline.
|
|
|
|
```bash
|
|
cd packages/producer
|
|
|
|
# Build the test Docker image
|
|
bun run docker:build:test
|
|
|
|
# Run regression tests (compares output against golden baselines)
|
|
bun run docker:test
|
|
|
|
# Regenerate golden baselines after intentional changes
|
|
bun run docker:test:update
|
|
```
|
|
|
|
## Benchmarking
|
|
|
|
Find optimal render settings for your hardware:
|
|
|
|
```bash
|
|
# Via the CLI
|
|
npx hyperframes benchmark
|
|
|
|
# Directly from the producer package
|
|
cd packages/producer
|
|
bun run benchmark
|
|
```
|
|
|
|
The benchmark runs several compositions with different quality and FPS settings and reports timing for each combination.
|
|
|
|
## External assets (files outside `projectDir`)
|
|
|
|
A composition can reference absolute paths to assets outside the project
|
|
directory — a local voiceover in `~/Downloads`, a shared-drive image, a
|
|
generated fixture at an absolute path. The producer handles these by:
|
|
|
|
1. **Detection.** During compilation, the HTML compiler walks every
|
|
`[src]` / `[href]` and every `url(...)` in `<style>`. A path that
|
|
resolves to a file outside `projectDir` is collected into an
|
|
`externalAssets` map.
|
|
2. **Sanitised keys.** Each absolute path is converted into a safe,
|
|
cross-platform relative key prefixed with `hf-ext/`. Windows
|
|
drive-letter colons are stripped (`D:\foo\x.wav` → `hf-ext/D/foo/x.wav`)
|
|
so that `path.join(compileDir, key)` stays inside the compile
|
|
directory on every OS.
|
|
3. **Copy + rewrite.** The orchestrator copies the file under
|
|
`<compileDir>/hf-ext/...` and the HTML is rewritten to point at the
|
|
sanitised key. The file server then serves both project-internal and
|
|
external assets from the same root.
|
|
|
|
The containment check uses `path.relative()` rather than a hardcoded
|
|
separator, so external assets work identically on macOS, Linux, and
|
|
Windows. See `packages/producer/src/utils/paths.ts` for the helpers.
|
|
|
|
## Related Packages
|
|
|
|
<CardGroup cols={2}>
|
|
<Card title="CLI" icon="terminal" href="/packages/cli">
|
|
Command-line interface that wraps the producer for rendering, previewing, and more.
|
|
</Card>
|
|
<Card title="Engine" icon="gear" href="/packages/engine">
|
|
The low-level capture pipeline that the producer uses to grab frames.
|
|
</Card>
|
|
<Card title="Core" icon="cube" href="/packages/core">
|
|
Types, runtime, and linter that the producer depends on.
|
|
</Card>
|
|
<Card title="Studio" icon="palette" href="/packages/studio">
|
|
Visual editor for building compositions before rendering with the producer.
|
|
</Card>
|
|
</CardGroup>
|