Files
Vance Ingalls e1f63a85e1 fix(engine): make software-renderer screenshot guard opt-in (CI regression)
PR #822's software-renderer guard auto-flipped `captureMode` to "screenshot"
whenever `resolveBrowserGpuMode` returned "software" — but the WebGL probe
classifies GitHub Actions runners (SwiftShader) as "software" too, even
though `HeadlessExperimental.beginFrame` works on them. As a result the
guard pessimized mainstream CI, exceeding `ffmpegStreamingTimeout` (10 min
default) on the largest fixtures and producing this regression on three
shards of run 25841853063:

  - fast → sub-composition-video (390 frames, 1080×1920, video + sub-comp)
  - styles-e → style-13-prod (12 MB baseline, slow tag)
  - styles-g → overlay-montage-prod (42.36 s duration, 1080×1920)

All three failed with `Streaming encode failed: FFmpeg exited with code 255
... received signal 15`. Wall-clock for overlay-montage-prod jumped from
9 m 13 s (main, beginframe) to 10 m 38 s (PR-822, screenshot) — just past
the streaming timeout. ffmpeg was alive and consuming frames the whole
time; the producer side simply couldn't sustain the rate in screenshot
mode for these fixtures.

Fix: gate the auto-flip behind an opt-in env var
`HYPERFRAMES_FORCE_SCREENSHOT_ON_SOFTWARE_GPU`. When unset (default), the
GPU probe is skipped entirely and the original (pre-PR-822) capture-mode
selection runs — Linux + headless-shell → beginframe, with the existing
`probeBeginFrameSupport` fallback handling actual BeginFrame unavailability
at runtime. Operators on hosts where BeginFrame is technically present but
stalls under shader load (the CPU-only Linux sandbox where hf#677 was
reproduced) set the env var to short-circuit BeginFrame entirely.

Mirrored the same gate in `frameCapture.ts`'s preMode resolution so the
expensive probe Chrome is also skipped on the chromeArgs side when the
guard is off.

Tests: kept all eight software-renderer tests; flipped the block-scoped
default to "guard ON" (set in `beforeEach`) so the pre-existing assertions
exercise the guard path. Added a positive-control test that pins the new
default behavior: `Linux + software + headless-shell + guard OFF →
beginframe` (the regression case).

— Vai
2026-05-14 07:36:20 +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