mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
* feat(core): hold render-ready on declared compute and a build-ready hold, not just duration Adds the compute and paint-and-idle inputs to the composition readiness gate, plus window.__hf.buildReady: a promise registry a piece registers when it has setup work no adapter can see (mesh building, shader compiles). The runtime holds render-ready on it and the player shows its loading state instead of a blank frame while it resolves. * refactor(core): dedupe the paint-and-idle readiness test setup Simplify pass on 23810da1b: fallow flagged the two paintAndIdleReadinessInput tests as a 12-line clone; extracted the shared doc/tracking setup. * fix(core): stop the readiness settled-tracker hanging on 2+ pending promises Promise.all allocates a new promise every call, so comparing ITS identity across repeat polls (isAdapterReadinessSettled, isBuildReadinessSettled) never observes settled once 2+ promises are combined: every poll rebuilds a new combined promise, re-arms, and republishes forever without the caller ever seeing true. Reproduced with two already-resolved window.__hf.buildReady entries, which is the exact multi-piece usage the new authoring doc describes. Fixes both trackers at once with a shared createSettledTracker that compares the source promise array by element instead of the Promise.all wrapper. * fix(core): cancel readiness inputs once settleCompositionReadiness settles computeReadinessInput's poll timer and paintAndIdleReadinessInput's rAF chain kept running after the shared timeout won the race, since nothing told the losing inputs to stop. One AbortSignal now threads through all three inputs and is aborted the instant the race settles. * docs(core): fix a stale sync-callback claim in settleCompositionReadiness paintAndIdleReadinessInput always has a frame to wait on for any real document, so the sync fast path this docstring described is no longer reachable with the default inputs except for a viewless document. Found by the simplify pass on this branch. * fix(core): clear a stale buildReady entry on runtime teardown A never-resolved window.__hf.buildReady promise from a torn-down composition survived into the next init in the same window (window.__hf is never reset), permanently blocking render-ready for whatever loaded next -- the exact black-screen class this branch fixes, reproduced through its own new mechanism. Found by the independent review. Also fixes a stale hyperframes-player.ts docstring (media/images/fonts had not been updated to compute + paint-and-idle) and documents the buildReady key-collision hazard in three.md. * fix(core): skip the compute readiness wait when no runtime ever ran computeReadinessInput polled window.__renderReady for the full shared timeout on any same-origin document with no HyperFrames runtime injected (a probe-only composition using native video/duration, or a blank test iframe) — nothing was ever going to set that flag. Checking window.__hf first (the same signal composition-probe.ts already treats as "has a runtime") lets it return null instead of waiting. * fix(player): debounce the loading overlay and diagnose compute timeouts paintAndIdleReadinessInput always has a frame to wait on for an attached iframe, so settleCompositionReadiness's synchronous-settle path is unreachable in practice now — the loading overlay showed on every single Play, even ones with nothing genuinely pending. Debounce the overlay itself instead: a wait that resolves fast never shows it. Also names compute (window.__renderReady) in the stuck-assets timeout diagnostic, which previously only re-scanned media, images and fonts. * fix(engine): name a stuck buildReady key in the zero-duration diagnostic A build promise a composition registers on window.__hf.buildReady that never resolves left the export path's zero-duration timeout naming nothing useful — hasHf/hasSeek/duration all looked fine, since the runtime was genuinely present and just waiting on that one promise. Probes buildReady's keys for pending ones and names them in the hint. * fix(engine): stop the buildReady diagnostic from crashing on a rejection A microtask race against a bare resolved promise rejected the whole diagnostic expression on any rejected buildReady entry (the runtime's own createSettledTracker rejects deliberately) and mis-sorted a settled non-native thenable as pending, since its extra adoption tick loses the race. Both measured directly, not inferred. Settle each key onto its own record past one macrotask instead: no throw, and pending/resolved/rejected are distinguished correctly. A rejected key gets its own hint, separate from a merely pending one. * fix(player): report a backgrounded document instead of overclaiming a paint bound MAX_PAINT_WAIT_MS only bounds a document that keeps producing frames; one that stops entirely (backgrounded tab suspends rAF, or fewer than 3 frames ever fire) still rides the full shared 8s timeout, same as before this input existed. Corrected the comments that claimed otherwise. That gap is exactly why the stuck-assets diagnostic left paint-and-idle unreported: there was no accurate way to say it might be the cause. Added documentHidden (doc.hidden) to the diagnostic instead, since a backgrounded tab is the reachable, directly observable case. * refactor(engine): simplify the buildReady diagnostic rewrite Skip the macrotask wait entirely when a composition registers no buildReady keys — the common case, since it's opt-in. Nothing to settle means nothing to wait on. Collapse buildZeroDurationDiagnostic's inline parameter type into the existing HfDiagnostic interface instead of maintaining two copies of the same shape. * docs(engine): note the setTimeout(0)-thenable gap the review found An independent adversarial review, executing hostile inputs rather than tracing, found the macrotask wait can still misreport a thenable that resolves via its own internal setTimeout(0), since the diagnostic's own timer is registered before the thenable's. Bounded and self-correcting (only feeds the message text, and the diagnostic re-runs every ~1s), so documenting the boundary rather than chasing an unbounded 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,
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