Files
hyperframes/packages/engine
Vance Ingalls 926c4c223e fix(engine): force screenshot capture when GPU is software (broader than HDR-only guard)
When the host has no hardware GPU (SwiftShader software rendering), Chrome's
`HeadlessExperimental.beginFrame` stalls the compositor on shader-heavy
frames — the CDP call hangs and the render eventually fails with
`beginFrame timed out` at protocolTimeout (default 5 min). This was
reproduced on a CPU-only Linux sandbox during the hf#677 BeginFrame spike
and is likely the same root cause as the earlier Mac-Docker timeout that
the HDR-only guard in `captureHdrStage.ts` partially mitigated.

The existing per-stage guard (`cfg.forceScreenshot = true` for the layered
HDR composite) is too narrow — the issue is GPU-presence, not HDR-vs-SDR.
SDR renders also stall on a software-renderer host.

Fix: in `acquireBrowser`, resolve `browserGpuMode` (cheap — the probe
Promise is cached for the process lifetime) and force
`captureMode = "screenshot"` whenever it resolves to "software", regardless
of platform or binary. Defensively strip BeginFrame-only chrome flags
(`--enable-begin-frame-control` etc.) from the launch args in the same
path — leaving them in screenshot mode produces blank captures because the
compositor waits for beginFrames the engine no longer sends.

Mirror the same gate in `frameCapture.ts`'s preMode computation so the
chromeArgs are built without BeginFrame flags up front (the strip in
`acquireBrowser` is belt-and-braces for any other caller).

A one-time `console.warn` documents the resolution path for operators
when the guard actually changed the outcome (Linux + headless-shell only —
silent on the always-screenshot platforms and on hardware hosts).

Note: on `main` as-is the layered HDR path already forces screenshot
mode unconditionally, so the SDR-software-renderer case is the load-bearing
one this PR fixes. The broader guard also keeps the engine safe once any
future BeginFrame-alpha work lands on top.

Tests cover: software → screenshot + BeginFrame flag stripping, hardware →
beginframe preserved, forceScreenshot=true still wins, one-time
software-GPU warning, no warn on macOS (already screenshot anyway).

— Vai
2026-05-14 01:51:26 +00:00
..
2026-05-13 17:01:15 -07:00
2026-03-21 22:43:56 -07:00

@hyperframes/engine

Seekable web-page-to-video rendering engine built on Puppeteer and FFmpeg.

Framework-agnostic: works with GSAP, Lottie, Three.js, CSS animations, or any web content that implements the window.__hf seek protocol.

Install

npm install @hyperframes/engine

Requirements: Node.js >= 22, Chrome/Chromium (auto-downloaded by Puppeteer), FFmpeg

What it does

The engine opens your HTML composition in a headless Chrome instance, seeks frame-by-frame using Chrome's HeadlessExperimental.beginFrame API, captures screenshots, and encodes them into video with FFmpeg.

Key services

Service Description
browserManager Launches and pools headless Chrome instances (chrome-headless-shell)
frameCapture Manages capture sessions — seek, screenshot, buffer lifecycle
screenshotService BeginFrame-based capture with CDP (Chrome DevTools Protocol)
chunkEncoder FFmpeg encoding with chunked concat, GPU detection, faststart
streamingEncoder Pipe frames to FFmpeg in real time (no intermediate PNGs on disk)
audioMixer Parse <audio> elements and mix audio tracks via FFmpeg
videoFrameExtractor Extract frames from <video> elements for compositing
parallelCoordinator Split frame ranges across worker processes
fileServer Serve local HTML files to the browser via Hono

Usage

import {
  acquireBrowser,
  releaseBrowser,
  createCaptureSession,
  initializeSession,
  captureFrame,
  closeCaptureSession,
} from "@hyperframes/engine";

// 1. Launch browser
const browser = await acquireBrowser({ captureMode: "beginFrame" });

// 2. Open a capture session
const session = createCaptureSession({
  browser: browser.browser,
  url: "http://localhost:3000/my-composition.html",
  width: 1920,
  height: 1080,
  fps: 30,
});
await initializeSession(session);

// 3. Capture frames
for (let i = 0; i < totalFrames; i++) {
  await captureFrame(session, i, `/tmp/frames/frame-${i}.png`);
}

// 4. Clean up
await closeCaptureSession(session);
await releaseBrowser(browser);

Most users should use @hyperframes/producer or the hyperframes CLI instead of calling the engine directly.

Documentation

Full documentation: hyperframes.heygen.com/packages/engine