Programmatic video generation in Node.js
A walkthrough of generating MP4s from a Node.js process — headless Chromium, frame capture, encoding, and the pitfalls to avoid.
Node is a reasonable place to put a video-generation pipeline. The same project that runs your Next.js app can drive headless Chromium, capture frames, and shell out to an encoder. The pieces are all npm install-able. The hard part isn't getting Node to do it; it's structuring the pipeline so the output is reproducible.
The components
A working pipeline has four:
- A renderer process. Usually
puppeteer-coreorplaywrightdriving headless Chromium. - An HTML scene with a
render(t)contract — see HTML to MP4 programmatically. - A frame capture loop. For each frame index, set
t, wait for paint, screenshot. - An encoder.
mp4-muxer(pure JS),h264-mp4-encoder(WASM), or shell out toffmpeg. All three work; pick by deployment target.
A skeleton
import puppeteer from "puppeteer-core";
import { Muxer, ArrayBufferTarget } from "mp4-muxer";
async function renderScene(url: string, durationSec: number, fps = 30) {
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 1 });
await page.goto(url, { waitUntil: "networkidle0" });
await page.evaluate(() => document.fonts.ready);
const total = Math.round(durationSec * fps);
const muxer = new Muxer({
target: new ArrayBufferTarget(),
video: { codec: "avc", width: 1920, height: 1080 },
fastStart: "in-memory",
});
for (let i = 0; i < total; i++) {
const t = i / fps;
await page.evaluate((time) => window.dispatchEvent(new CustomEvent("hf-seek", { detail: { time } })), t);
await page.evaluate(() => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))));
const buf = await page.screenshot({ type: "png", omitBackground: false });
muxer.addVideoChunk(toEncodedChunk(buf, i, fps));
}
muxer.finalize();
await browser.close();
return muxer.target.buffer;
}The toEncodedChunk step is the part most tutorials hand-wave. For a production pipeline, route the PNGs through an h.264 encoder (@ffmpeg/ffmpeg, a native binding, or a CDP-based VideoEncoder).
Watch out for
Concurrency. Don't fan out frames within a single page; the DOM is one shared state. Fan out across multiple browser contexts. Empirically, 4-8 contexts per CPU core is the sweet spot for 1080p.
Memory. PNGs from page.screenshot are 6-8 MB at 1080p. A 60-second 30fps render is 1800 frames × 7 MB = 12 GB if you hold them all. Stream them through the muxer, don't accumulate.
Fonts. document.fonts.ready is necessary but not sufficient — variable fonts can shift their axis values after first paint. Either inline fonts as data URLs or wait an extra 100ms after fonts.ready for variable axes to settle.
Locale. toLocaleString reads the OS locale. A CI runner and a developer machine produce different numbers ("1,234" vs "1.234"). Force the locale at the top of every scene: Intl.NumberFormat("en-US", ...).
Where to deploy
For low-volume, on-demand rendering (one video per user request), a single Node + Chromium process per box is fine. For batch (thousands of videos), put Chromium in a container, ship the container to a queue worker, and parallelize.
For one-off long videos (10+ minutes), parallelism matters more than throughput. Split into chunks, render in parallel, concatenate.
The detailed CI-render story is in deterministic video rendering in CI. The headless-Chromium tuning is in headless Chrome video rendering.
A working summary
Node + headless Chromium + a deterministic HTML scene + an encoder is the smallest viable video-generation stack. It fits in a single repo, runs in CI, scales out per worker, and produces MP4s that are byte-identical between runs. None of the pieces are exotic; the discipline is in keeping wall-clock time out of the rendering loop.
Cite this postBibTeX · APA · Markdown
@misc{tanaka2026programmatic,
author = {Kira Tanaka},
title = {Programmatic video generation in Node.js},
year = {2026},
url = {https://hyperframes.video/blog/programmatic-video-generation-nodejs},
note = {HyperFrames blog}
}Kira Tanaka. (2026, May 19). Programmatic video generation in Node.js. HyperFrames. https://hyperframes.video/blog/programmatic-video-generation-nodejs
[Programmatic video generation in Node.js](https://hyperframes.video/blog/programmatic-video-generation-nodejs) — Kira Tanaka, 2026
Kira works on the render core: headless Chromium scheduling, frame capture, and the encoder pipeline. She cares about reproducible builds and small numbers next to the word "variance."
Generate an MP4 from a React component
Render a React component to a deterministic MP4. The trick is treating time as a prop, not a side effect.
Recording video with Puppeteer (and what to use instead)
Puppeteer can record video — sort of. Here's the screencast API, its limits, and the deterministic alternative for real production work.
How to convert HTML to MP4 programmatically
An end-to-end recipe for turning a static HTML file into a deterministic MP4 — no timeline tool, no manual export, no headless-Chrome plumbing.
Building with HyperFrames? Come hang out.
We're on GitHub, in Discord, and the playground is one click away. Bring weird ideas — we collect them.