feat(browser-export): 100% client-side MP4/WebM export via WebCodecs (mediabunny) - #1905
feat(browser-export): 100% client-side MP4/WebM export via WebCodecs (mediabunny)#1905HackingCorp wants to merge 1 commit into
Conversation
…(mediabunny) New @hyperframes/browser-export package (discussion in heygen-com#1661): deterministic quantized GSAP seeks (producer parity contract), SVG foreignObject rasterization (html-to-image), OfflineAudioContext mix of data-start/data-duration audio clips, WebCodecs encoding via mediabunny (avc+aac MP4, vp9+opus WebM). Browser-only tsup build (platform: "browser" compile-time guarantee), 26 unit tests on the pure logic, docs page + build-order registration.
miguel-heygen
left a comment
There was a problem hiding this comment.
I tested the new package locally and in headless Chrome. The simple video-only browser export does produce a WebM Blob, but I found two correctness issues that can make browser exports differ from HyperFrames' existing runtime/producer semantics.
Audited: packages/browser-export/src/* end-to-end, package/docs wiring, monorepo build, browser smoke for export + audio collection.
Trusting: lockfile dependency resolution details beyond frozen install/build.
Strengths:
packages/browser-export/src/frameCapture.ts:26caches embedded font CSS once instead of re-fetching fonts per frame, which is the right shape for frame-by-frame capture.packages/browser-export/src/exporter.ts:85keeps frame progress and frame encoding in one deterministic loop, which made the browser smoke straightforward to exercise.
Blockers:
packages/browser-export/src/exporter.ts:73—prepareAudioscopes audio discovery toplan.root.ownerDocument, so exporting one composition from an editor page mixes/fetches every<audio>/<video>in the whole document, not just media inside the composition root. I reproduced this in Chrome withexportComposition(document.getElementById("root")): a sibling<audio src="/outside.wav">outside#rootwas fetched during export. That will leak editor/shell media into exports; this should collect fromplan.root(and preserve the producer media contract) instead of the owner document.packages/browser-export/src/timelineSeek.ts:52—seekTimelinesseeks every registered timeline to the same absolute frame time. Existing render/player code activates sibling timelines but seeks the root/master timeline; it explicitly avoids absolute-seeking registered child timelines because nested/sub-composition timelines are offset-relative and the root seek already propagates their local time. This implementation can overwrite that propagated child state and render sub-compositions at the wrong frame. The helper should mirror the runtime render seek path rather than iteratingObject.values(registry)with absolutet.
Verification:
bun install --frozen-lockfilebun run --filter @hyperframes/browser-export test(26 passed)bun run --filter @hyperframes/browser-export typecheckbun run --filter @hyperframes/browser-export buildbunx oxlint packages/browser-export/src packages/browser-export/tsup.config.tsbunx oxfmt --check packages/browser-export/src packages/browser-export/tsup.config.ts packages/browser-export/package.json packages/browser-export/tsconfig.json package.json docs/docs.jsonbun run build- Puppeteer/Vite browser smoke: video-only export returned
video/webm, 1 frame, 563-byte Blob; audio-enabled scoped repro fetched/outside.wavfrom outside the composition root.
Verdict: REQUEST CHANGES
Reasoning: The basic browser encode path works, but audio collection and timeline seeking currently diverge from the established HyperFrames contracts in realistic editor/nested-composition cases.
— Codex
|
Closing — this PR has merge conflicts and has been inactive. Thank you for the contribution! Please feel free to reopen with a rebased branch if you'd like to continue. |
Summary
Implements the browser-side export path discussed in #1661: a new
@hyperframes/browser-exportpackage that renders a live composition to MP4/WebM entirely in the browser — no server, no FFmpeg, no headless Chrome. It complements@hyperframes/producer(which remains the reference for deterministic, pixel-perfect renders); the target is browser-based editors, template SaaS frontends, and quick exports delivered straight to the user's Downloads folder.Pairs naturally with
@hyperframes/lint/browser(#1749/#1773) for a fully client-side validate → render loop.How it works
[data-composition-id]or#root), read dimensions, resolve duration from the master GSAP timeline inwindow.__timelines(oroptions.duration).<audio>/<video>clip metadata (data-start,data-duration/data-end,data-media-start,data-volume— the producer's contract), decode and mix offline withOfflineAudioContext.Math.round(t·fps)/fps, the producer's parity contract), await<video>layer seeks, rasterize the root to a canvas via SVGforeignObject(html-to-image, fonts embedded once).avc+aacin MP4,vp9+opusin WebM), finalized into a downloadableBlob. Abortable viaAbortSignal, per-phaseonProgress.Package shape
platform: "browser"build — the same compile-time node-free guarantee as@hyperframes/lint/browser.mediabunny(already used by studio's mediaProbe) +html-to-image.<video>frame alignment with a 500 ms guard, main-thread rendering.Testing
🤖 Generated with Claude Code
https://claude.ai/code/session_018TXJXMe4QmSBnmYoR3brtk