Files
hyperframes/docs/packages/producer.mdx
T
Miguel Ángel e8a48a62d0 fix(producer): external assets work on Windows (GH #321) (#324)
* fix(producer): external assets work on Windows (GH #321)

Two Unix-only assumptions in the external-asset pipeline caused every
absolute path on Windows to be rejected as "unsafe" at render time:

1. Containment checks used `child.startsWith(parent + "/")`. On Windows
   the separator is `\`, so the predicate is always false unless the
   paths are equal — every external asset tripped the safety guard in
   `renderOrchestrator.ts`. The reporter saw:

     [Render] Skipping external asset with unsafe path:
       hf-ext/D:\coder\reactGin\hyperframes\reading\assets\segment_001.wav

   Fix: use `path.relative()` through a shared helper
   `isPathInside(child, parent)` that normalises separators per-platform
   and correctly rejects siblings whose names start with the parent
   (e.g. `/foo/bar-sibling` is NOT inside `/foo/bar`).

2. The external-asset key was built as `"hf-ext/" + absPath.replace(/^\//, "")`.
   A Windows absolute path (`D:\coder\...`) became
   `"hf-ext/D:\\coder\\..."` — and because Node's `path.join` treats a
   drive-letter prefix as absolute, `join(compileDir, key)` silently
   escaped `compileDir`. Fix: `toExternalAssetKey()` strips the drive
   colon and normalises to forward slashes, producing
   `hf-ext/D/coder/...` — a pure relative path that `path.join` cannot
   promote to absolute on any OS.

Both helpers live in `packages/producer/src/utils/paths.ts` and are
exercised by 14 unit tests covering Unix paths, Windows drive-letter
paths, mixed separators, sibling-prefix confusion, and `..` traversal.

Docs: new "External assets" section in `docs/packages/producer.mdx`
describes detection, sanitised keys, and the cross-platform containment
invariant.

Closes #321.

* fix(producer): address review on #324 — UNC + integration test

Addresses the non-blocking observations from the PR #324 staff review
(https://github.com/heygen-com/hyperframes/pull/324#issuecomment):

1. UNC and extended-length Windows paths.
   `toExternalAssetKey` now handles:
   - `\\?\D:\very\long\path\clip.mp4` (extended-length)      → `hf-ext/D/very/long/path/clip.mp4`
   - `\\server\share\file.wav` (plain UNC)                    → `hf-ext/unc/server/share/file.wav`
   - `\\?\UNC\server\share\file.wav` (extended-length UNC)   → `hf-ext/unc/server/share/file.wav`
   The UNC-collapsed form keeps the server boundary so two different
   servers exposing the same share/file name cannot collide under one
   relative key. Previously both edge cases silently produced keys with
   stray `?` or `:` characters that downstream `isPathInside` rejected —
   not a security hole, but a silent drop of user assets.

2. Short-circuit on already-sanitised input.
   `toExternalAssetKey("hf-ext/…")` now returns its input unchanged
   instead of prepending `hf-ext/` a second time. Makes the helper
   genuinely idempotent, which is what the unit test claimed all along.
   Renamed the test accordingly.

3. JSDoc caller contract.
   `toExternalAssetKey` now documents that it expects canonicalised
   input (`path.resolve`'d upstream) and does not strip `..`
   components. `isPathInside` at copy time is still the defensive
   backstop — called out explicitly in the doc so future callers read
   the contract before the code.

4. End-to-end integration test.
   `renderOrchestrator.test.ts` gains two seam tests that run the full
   external-asset pipeline — build the sanitised key, populate an
   `externalAssets` map, invoke `writeCompiledArtifacts`, and assert
   both the success path (the file lands under `<compileDir>/hf-ext/…`)
   and the escape-rejection path (a malicious `hf-ext/../../etc/passwd`
   key does NOT materialise above `compileDir`). `writeCompiledArtifacts`
   is exported for the test seam with a clear JSDoc disclaimer that
   it's not part of the public API.

22 tests pass across `paths.test.ts` (17) and `renderOrchestrator.test.ts` (5).

Out of scope for this follow-up (tracked as follow-ups):
- Centralising every `startsWith("/")` absolute-path check into a
  shared helper across htmlCompiler / audioExtractor / audioMixer /
  videoFrameExtractor. Mentioned in the review; touches 5 files and
  deserves its own PR.
- Windows CI runner.
2026-04-18 20:59:51 +02:00

295 lines
10 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)
### 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>