mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
Review finding 1, the last of the fifteen. A group's identity was the raw author `id`, which is unique only per composition FILE — and the render document is the inlined union of every file. So a sub-composition declaring a bus AND its members, used twice, put both instances' members under one key: one sub-mix for two independent buses, and the second bus element overwrote the first, applying its fader, chain, label and automation to the first instance's audio. With only the SECOND instance muted, `memberGroupHidden` dropped every member of the merged group — both instances gone from the export. Compiled a twice-used sub-composition to find out what actually separates the two instances, rather than guessing: - members are ALREADY disambiguated — `data-hf-render-id="m1"` / `"m1__hf2"` - buses are not: `MEDIA_SELECTOR` is `video[src], audio[src], img[src]` - both instances carry `data-composition-id="bedcomp"`, the file's own id, so id strings cannot tell them apart — only the subtree element can Fixed at the boundary that already owns this collision class. `assignMediaRenderIds` now stamps every `<hf-audio-group>` with a document-unique `data-hf-render-id` from the SAME `taken` set (so a bus key can never collide with a clip key either), and stamps each member with `data-hf-group-render-id` = the render id of the bus in its OWN composition subtree, resolved with `closest()`. A member whose bus is not in its subtree — a hand-authored bus in the root with members in scenes — falls back to the first, which is the pre-existing reading and the only sensible one there. `resolveAudioGroups` and the mixer prefer the stamped key and fall back to the author id, so the LIVE PREVIEW — which has no stamps — reads exactly as before. `resolveGroupElement` tries the stamped instance first, since `getElementById` can only ever find the author id. Verified with the reviewer's own repro, end to end through the real compiler: before, `parseAudioElements` returned both members under one `bed` group; after, muting instance B drops only B's member and A survives at its own 0.5 fader. Two compiler tests (instance pairing, single-instance stability, element-less group untouched) and two mixer tests, all verified against a revert. **Still divergent, deliberately: the live preview.** `groupInput` resolves by id against the uncompiled document, so two instances still share one bus there. The export was the audible bug — a muted instance silencing another's audio — and fixing preview needs runtime subtree resolution, which is a separate change. core 2493, engine 1617, studio 4389. fallow clean.
@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,
createCaptureSession,
initializeSession,
captureFrame,
closeCaptureSession,
} from "@hyperframes/engine";
// 1. Launch browser
const browserLease = await acquireBrowser({ captureMode: "beginFrame" });
// 2. Open a capture session
const session = createCaptureSession({
browser: browserLease.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 browserLease.release();
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