Files
hyperframes/packages/engine
Miguel Ángel 3a2f052889 fix(engine): pad odd output dimensions up to even for H.264/H.265 encode (#1802)
* fix(engine): pad odd output dimensions up to even for H.264/H.265 encode

A composition with an odd data-width or data-height (e.g. a custom 3:1
canvas at 1080x723) failed to encode to MP4. libx264/libx265 with 4:2:0
chroma subsampling (yuv420p, yuv420p10le) require both dimensions to be
even and abort before writing a packet:

  [libx264] height not divisible by 2 (1080x723)
  Error while opening encoder ... Invalid argument

Both the streaming encoder and the chunk encoder built the software
range-conversion filter ("scale=in_range=pc:out_range=tv") with no
even-dimension enforcement, so any odd-sized canvas reached libx264
unmodified and the whole render failed.

Add a shared withEvenDimensionPad helper that appends
pad=ceil(iw/2)*2:ceil(ih/2)*2 to the filter chain only for 4:2:0 pixel
formats. The pad rounds each odd dimension up by one pixel (a no-op when
already even) without scaling, so content is never resampled. Formats
that accept odd dimensions (ProRes 4444 yuva444p10le, VP9 yuva420p) are
excluded, so transparent/alpha output is untouched.

* fix(engine): extend even-dimension pad to GPU 4:2:0 encode paths

The odd-dimension pad added for libx264/libx265 only covered the software
encoder branches. nvenc, videotoolbox, qsv, and amf feed software frames
straight to the hardware encoder with no -vf chain, so an odd-sized 4:2:0
canvas on --gpu (or an auto-selected hardware encoder) reproduced the same
"height not divisible by 2" abort before any packet was written.

Add the even-dimension pad to the software-side -vf chain for those four
GPU paths in both the chunk and streaming encoders, reusing the shared
withEvenDimensionPad helper (the pad runs on CPU before the encode). vaapi
is left as-is: its existing format=nv12,hwupload conversion already aligns
odd dimensions before upload, so it is not double-padded. ProRes 4444 and
VP9 alpha stay untouched, exactly as the software fix excludes them.

nvenc/videotoolbox/qsv/amf arg construction is logic-tested (the pad filter
is asserted on the built arg list for 8-bit and 10-bit 4:2:0, with alpha
ProRes asserted padless); runtime hardware encode is not exercised here.
2026-06-30 10:55:22 -07:00
..
2026-06-29 21:13:39 -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