mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-14 18:01:20 +08:00
b3cc92d86d
Four related bugs in the opacity pipeline that interact with HDR video
compositing and GSAP-controlled fades:
1A. screenshotService injectVideoFramesBatch and syncVideoFrameVisibility
were applying `opacity: 0 !important` to native <video> elements to
hide them under the injected <img>. That stomp clobbered any
GSAP-controlled inline opacity, so the next seek read 0 from
computed style and the comp went black. We now use
`visibility: hidden !important` only — visibility hides the element
from rendering without changing its opacity, so subsequent reads
(and queryElementStacking) see the real GSAP value on every frame.
The `parseFloat(...) || 1` recovery hack at injectVideoFramesBatch
was specifically there to compensate for this stomp; it’s now
replaced with a `Number.isNaN` guard that defaults to 1 only when
parsing actually fails.
1B. queryVideoElementBounds parsed `style.opacity` and `style.zIndex`
with `parseFloat(...) || N`, which silently coerces a real opacity
of 0 into 1 (and zIndex 0 into 0 only by coincidence). Switched to
explicit `Number.isNaN` checks so opacity 0 stays 0.
1C. resolveRadius cast `el as HTMLElement` to read offsetWidth/Height.
SVG and other non-HTML elements would have crashed at runtime.
Replaced the cast with an `instanceof HTMLElement` guard, and made
the numeric fallback `Number.isNaN`-safe.
1D. The opacity walk in queryVideoElementBounds started from
`el.parentElement` for HDR videos to skip past the engine’s forced
`opacity: 0` on the element itself. Now that the engine never sets
opacity, the special case is unnecessary — always walk from `el`.
Kept the `isHdrEl` lookup because transform/border-radius logic
further down still branches on it.
Verified:
- bun run --filter @hyperframes/engine typecheck (clean)
- bun run --filter @hyperframes/engine test (308/308 passing)
- bun run --filter @hyperframes/producer typecheck (clean)
- oxlint + oxfmt --check on both touched files
Next: regenerate hdr-regression window C (the opacity-fade window) and
tighten its maxFrameFailures budget — done in a follow-up commit so the
golden churn is reviewable separately from the code fix.
@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
Related packages
@hyperframes/core— types, parsers, frame adapters@hyperframes/producer— high-level render pipeline built on this enginehyperframes— CLI