Part of Phase 2 of the distributed rendering plan (determinism hardening). See DISTRIBUTED-RENDERING-PLAN.md §4.3 (LockedRenderConfig.runtimeEnv) and §5.2 (RENDER_SEEK_MODE row). `fileServer.ts` reads several `PRODUCER_RUNTIME_*` and `PRODUCER_RENDER_*` env vars at module-load time (RENDER_SEEK_MODE, RENDER_SEEK_STEP, RENDER_SEEK_OFFSET_FRACTION, …) and bakes them into the served HTML's RENDER_MODE_SCRIPT. Distributed chunk workers are separate processes that may inherit a different environment, so the plan needs to freeze a snapshot. Adds `snapshotRuntimeEnv(env = process.env)` in packages/producer/src/services/render/stages/freezePlan.ts. Captures keys matching `PRODUCER_RUNTIME_` or `PRODUCER_RENDER_` prefixes into a fresh plain object, ignoring everything else. Phase 3's `renderChunk` will materialize the snapshot back into `process.env` before launching its file server. Also exports `RUNTIME_ENV_SNAPSHOT_PREFIXES` so the chunk-worker side can apply the same prefix filter (asymmetric handling would leak stale controller env into worker behavior). The freezePlan function body remains a skeleton — Phase 3 owns the full implementation. The snapshot helper is exported on its own so this gate's unit test can pin the behavior without depending on the not-yet-written freezePlan body. In-process behavior is unchanged: no in-process caller invokes freezePlan or snapshotRuntimeEnv yet. 9 unit tests at packages/producer/src/services/render/stages/ freezePlan.test.ts cover: prefix matches (both families), non-matching keys ignored, undefined values skipped, fresh-object contract, and default-to-process.env behavior. This is part of a stack of 10 PRs; this is PR 5 of 10. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hyperframes/producer
Full HTML-to-video rendering pipeline: capture frames with Chrome's BeginFrame API, encode with FFmpeg, mix audio — all in one call.
Install
npm install @hyperframes/producer
Requirements: Node.js >= 22, Chrome/Chromium (auto-downloaded), FFmpeg
Usage
Render a video
import { createRenderJob, executeRenderJob } from "@hyperframes/producer";
const job = createRenderJob({
inputPath: "./my-composition.html",
outputPath: "./output.mp4",
width: 1920,
height: 1080,
fps: 30,
});
const result = await executeRenderJob(job, (progress) => {
console.log(`${Math.round(progress.percent * 100)}%`);
});
console.log(result.outputPath); // ./output.mp4
Run as an HTTP server
The producer can also run as a render server, accepting render requests over HTTP:
import { startServer } from "@hyperframes/producer";
await startServer({ port: 8080 });
// POST /render with a RenderConfig body
Configuration
RenderConfig controls the render pipeline:
| Option | Default | Description |
|---|---|---|
inputPath |
— | Path to the HTML composition |
outputPath |
— | Output video file path (or directory, for format: "png-sequence") |
width |
1920 | Frame width in pixels |
height |
1080 | Frame height in pixels |
fps |
30 | Frames per second (24, 30, or 60) |
quality |
"standard" |
Encoder preset ("draft", "standard", "high") |
format |
"mp4" |
Output container — "mp4", "webm", "mov", or "png-sequence". See Transparent Video Output below. |
Transparent Video Output
The producer can render HTML compositions to formats that carry a true alpha channel — not chroma key. The same composition that renders an opaque MP4 renders a layerable overlay when you set format.
format |
Codec / pixel format | Alpha | Audio | Use case |
|---|---|---|---|---|
"mp4" (default) |
H.264 (yuv420p) or H.265 + HDR10 | No | AAC | Streaming, social, default deliverable |
"webm" |
VP9 + yuva420p | True alpha | Opus | Web playback as overlay (<video> over background); supported in Chrome, Edge, Firefox |
"mov" |
ProRes 4444 + yuva444p10le | True alpha + 10-bit | AAC | Editor ingest (Premiere, Final Cut Pro, DaVinci Resolve) |
"png-sequence" |
Numbered RGBA PNGs in a directory | Lossless alpha | Sidecar audio.aac |
After Effects / Nuke / Fusion, or pipelines that post-process frames before encoding |
Example
import { createRenderJob, executeRenderJob } from "@hyperframes/producer";
const job = createRenderJob({
inputPath: "./my-composition.html",
outputPath: "./output.webm", // or a directory for "png-sequence"
width: 1080,
height: 1920,
fps: 30,
format: "webm", // "mp4" | "webm" | "mov" | "png-sequence"
});
await executeRenderJob(job);
What "transparent background" means here
The producer captures Chrome screenshots with the page background forced transparent (html, body, [data-composition-id] { background: transparent !important }) and the CDP default background override set to RGBA 0,0,0,0. The captured PNGs carry a real alpha channel and that channel is preserved end-to-end:
- VP9 (
webm) is encoded with-pix_fmt yuva420p,-auto-alt-ref 0, andalpha_mode=1metadata. - ProRes 4444 (
mov) is encoded with-pix_fmt yuva444p10le. - PNG sequences are written without re-encoding (zero-padded
frame_NNNNNN.png).
This is not chroma keying. There is no green/blue background to remove and no "key" tolerance to tune — pixels that were transparent in the browser are transparent in the output.
Caveats
- Linux + alpha forces screenshot capture. Chrome's BeginFrame compositor (the default deterministic capture path on Linux headless-shell) does not preserve alpha; the orchestrator falls back to
Page.captureScreenshot, which is slower per frame. macOS and Windows already use screenshot mode by default, so they are unaffected. - HDR + alpha is not supported. Setting
hdr: truetogether with an alpha-capable format logs a warning and falls back to SDR. Useformat: "mp4"for HDR10 output. png-sequencedoes not produce a single muxed file. When the composition contains audio elements, anaudio.aacsidecar is written alongside the PNGs inoutputPath.- Safari + WebM alpha is incomplete. For broad browser playback of an alpha video, ship
format: "mov"to your editor and re-encode for the codec your distribution target supports.
Authoring transparent compositions
Don't paint a fullscreen background in your HTML. The default body background is overridden to transparent automatically — any body { background: ... }, #root { background: ... }, or [data-composition-id] { background: ... } rule is force-overridden during alpha rendering. Backgrounds on inner elements (cards, scenes, components) are kept.
How it works
- Serve — spins up a local file server for the HTML composition
- Capture — opens the page in headless Chrome, seeks frame-by-frame via
HeadlessExperimental.beginFrame(orPage.captureScreenshotfor transparent / non-Linux renders), captures screenshots - Encode — pipes frames through FFmpeg (with GPU encoder detection and chunked concat). Skipped for
format: "png-sequence". - Mix — extracts
<audio>elements and mixes them into the final video. Forpng-sequence, audio is written as anaudio.aacsidecar. - Finalize — applies faststart for streaming-friendly MP4 (no-op for WebM, MOV, and
png-sequence)
Documentation
Full documentation: hyperframes.heygen.com/packages/producer
Related packages
@hyperframes/core— types, parsers, frame adapters@hyperframes/engine— lower-level capture and encode primitiveshyperframes— CLI