@hyperframes/producer
v0.8.217
Published
HTML-to-video rendering engine using Chrome's BeginFrame API
Readme
@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/producerRequirements: Node.js >= 22, Chrome/Chromium (auto-downloaded), FFmpeg
Usage
Render a video
import { createRenderJob, executeRenderJob } from "@hyperframes/producer";
const job = createRenderJob({
fps: 30,
quality: "standard",
});
await executeRenderJob(job, "./my-project", "./output.mp4", (currentJob) => {
console.log(`${Math.round(currentJob.progress)}%`);
});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 or /render/stream with projectDir, html, or previewUrl (public HTTPS only) and render optionsBoth HTTP routes accept optional pageMotionBlur: "off" to disable authored page
motion blur. Omit it to preserve the composition's behavior. Other explicit values
are rejected before a render starts. This is separate from engine sub-frame
motionBlur sampling.
Configuration
RenderConfig controls the render pipeline:
| Option | Default | Description |
| ------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| entryFile | index.html | Entry composition inside the project directory. Its authored dimensions set the canvas size. |
| fps | required | Frame rate as an integer or exact { num, den } rational. |
| quality | required | Encoder preset ("draft", "standard", "high"). |
| pageMotionBlur | authored setting | Set "off" to disable page motion blur; omit to preserve authored behavior. |
| format | "mp4" | Output container — "mp4", "webm", "mov", "gif", "png-sequence", or "hls". See Transparent Video Output and HLS Output below. |
| hlsSegmentSeconds | 4 | HLS target segment length in whole seconds. Ignored for every other format. |
| videoFrameFormat | "auto" | Source video frame extraction format — "auto", "jpg", or "png". Use "png" for UI recordings, screen captures, and color-sensitive source videos. |
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 | AAC | Editor ingest (Premiere, Final Cut Pro, DaVinci Resolve) |
| "png-sequence" | Numbered RGBA PNGs in a directory | Lossless alpha | Sidecar audio.m4a | After Effects / Nuke / Fusion, or pipelines that post-process frames before encoding |
| "hls" | H.264 (yuv420p) in MPEG-TS | No | AAC rendition | VOD streaming — see HLS Output |
Example
import { createRenderJob, executeRenderJob } from "@hyperframes/producer";
const job = createRenderJob({
fps: 30,
quality: "standard",
format: "webm", // "mp4" | "webm" | "mov" | "png-sequence"
});
await executeRenderJob(job, "./my-project", "./output.webm");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,-cpu-used 4by default, andalpha_mode=1metadata. Tune the speed/quality tradeoff withPRODUCER_VP9_CPU_USED(-8to8) or local CLI--vp9-cpu-used. - ProRes 4444 (
mov) uses-pix_fmt yuva444p10leand carries a separate alpha plane. Chrome's RGBA capture, including alpha, is 8 bits per channel; the 10-bit pixel format adds no capture precision. - 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.m4asidecar 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.
HLS Output
format: "hls" packages the same H.264 + AAC encode as mp4 as an HLS VOD
stream. outputPath is a directory:
master.m3u8 # references both renditions
video.m3u8 # video media playlist
video_00000.ts # MPEG-TS segments, one IDR frame at the start of each
audio.m3u8 # written only when the composition has audio
audio_00000.tsconst job = createRenderJob({
fps: 30,
quality: "standard",
format: "hls",
hlsSegmentSeconds: 4,
});
await executeRenderJob(job, "./my-project", "./renders/stream");Segments are exactly hlsSegmentSeconds long except the last: the encoder's GOP
is locked to round(fps × hlsSegmentSeconds) frames so the packaging pass can
stream-copy (-c copy) and still cut on keyframes. There is no second encode.
Constraints:
- SDR only.
hdrMode: "force-hdr"is rejected; HDR10 would need fMP4 segments and HEVC. Useformat: "mp4"for HDR. - Software encoder only.
useGpu: trueis rejected — GPU encoders ignore the forced-keyframe lock that fixed-length segments depend on. - Not available in distributed rendering, including Lambda and Cloud Run.
- MPEG-TS carries no mov-family metadata, so the render provenance tags written into MP4 output are absent.
Distributed rendering
For renders too large for a single machine, the producer ships a public set of distributed-render primitives. They are pure functions over local file paths — networking and orchestration live in adapter packages (Temporal, AWS Lambda + Step Functions, Cloud Run Jobs, K8s Jobs).
Plan v2 is recommended for new integrations. It publishes an immutable manifest plus content-addressed artifacts and materializes only each worker's declared dependencies:
import { planV2, renderChunkV2, assembleV2 } from "@hyperframes/producer/distributed";
// Controller-side: produce a v2 manifest + local content-addressed store.
const planResult = await planV2(
projectDir,
{ fps: 30, width: 1920, height: 1080, format: "mp4" },
"/tmp/plan-v2",
);
const chunk = await renderChunkV2("/tmp/plan-v2", 0, "/tmp/chunks/0.mp4");
// Controller-side: stitch chunks into the final deliverable.
await assembleV2("/tmp/plan-v2", ["/tmp/chunks/0.mp4", "/tmp/chunks/1.mp4"], "/tmp/output.mp4");Cloud adapters should use planV2WithPublisher() so artifacts publish
directly to object storage. The legacy plan() / renderChunk() /
assemble() v1 layout remains supported, and cloud SDKs still interpret an
omitted protocol as v1 for backwards compatibility.
The activity functions plus their result types are also re-exported from @hyperframes/producer so callers that pin the main package don't need a separate subpath import. Supported formats: mp4 SDR, mov ProRes 4444, and png-sequence. webm and HDR mp4 trip a typed FormatNotSupportedInDistributedError — use the in-process renderer (executeRenderJob) for those.
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.m4asidecar. - Finalize — applies faststart for streaming-friendly MP4 (no-op for WebM, MOV, and
png-sequence). Forhls, stream-copies the encode into playlists and MPEG-TS segments instead.
Where each step lives
executeRenderJob in src/services/renderOrchestrator.ts runs the stages in
src/services/render/stages/, in this order:
compileStage.ts,probeStage.ts,extractVideosStage.ts,audioStage.tssrc/services/render/captureCost.tssizes the worker count (runCaptureCalibration, thenresolveRenderWorkerCount)createCapturePlaninsrc/services/render/capturePlan.tspicks the capture kind: streaming, disk, segmented or HDR layered- capture:
captureStreamingStage.ts,captureSegmentedStage.tsandcaptureHdrStage.tsencode as they capture; disk capture (captureStage.ts) encodes afterwards inencodeStage.ts assembleStage.ts, except for PNG sequences and GIF, which are written directly
The job's producerConfig is used when present; otherwise resolveConfig in
packages/engine/src/config.ts builds it. Capture, encoding and browser
primitives are engine services (see packages/engine/README.md).
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
