window.open with noopener always returns null to the opener per spec,
which caused the POPUP_BLOCKED fallback to always trigger. Removed
noopener so the opener gets a window reference for popup-blocked detection.
Note: without noopener, popup windows share the opener's event loop
in same-origin contexts, limiting parallelism. True multi-process
parallelism requires cross-origin isolation or future APIs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After seek, force offsetHeight reflow on all nested iframes (not just
the main document). Sub-compositions have their own GSAP timelines in
separate documents — without reflowing them, transform values (x, y)
captured by SnapDOM can be stale between frames, causing text/element
position jumps.
Also reorders the yield-before-reflow (let runtime propagate seek to
sub-compositions first, then reflow everything at once).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Seek to 0.1s first to trigger sub-composition iframe loading
- Wait for fonts.ready in all sub-composition iframes (not just main)
- Increase font timeout to 5s for CDN fonts
- Add more settle ticks (10x10ms) for complex GSAP nested timelines
- Double-seek to 0 after warmup for clean first frame
- Increase per-frame yield from 0ms to 4ms for better GSAP sync
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Opens N browser tabs (via window.open with noopener) for true
multi-process parallelism. Each tab gets its own Chromium renderer
process with independent main thread, bypassing the single-thread
SnapDOM bottleneck.
Architecture:
- TurboPool coordinator opens N minimized offscreen tabs
- Each tab loads composition via SnapdomFrameSource independently
- Frames captured as PNG, sent via BroadcastChannel to coordinator
- Coordinator decodes to ImageBitmap, feeds reorder buffer + encoder
- Concurrency: min(cores/2, 6), capped at 6 tabs
- Falls back to normal IframePool if popups blocked
Also:
- Add turbo toggle (lightning bolt) to Studio render UI
- Add /api/projects/:id/renders/upload endpoint for saving
browser-rendered videos to project renders directory
- Export isTurboSupported() from @hyperframes/renderer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Discover media elements from DOM when __player bridge doesn't expose hf.media
(scans both <video> and <audio> elements with data-start/data-duration)
- Resolve media src to absolute URLs so audio mixer fetch() works cross-origin
- Seek original video elements directly instead of clone+canvas overlay approach
(SnapDOM natively captures <video> via drawImage)
- Force synchronous reflow after GSAP seek to prevent stale computed styles
- Add warmup phase: wait for fonts.ready + settle ticks before first capture
- Replace all requestAnimationFrame with setTimeout for background tab support
- Fix interleaved capture/encoding progress by deferring encoding reports
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- iframe-pool: setTimeout(0) yield after each captured frame
- SnapDOM source: requestAnimationFrame after seek() before capture
to ensure layout/paint is processed
This prevents Chrome from becoming unresponsive during long renders,
keeping CDP, DevTools, and UI interactions functional.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SnapDOM frame source now detects window.__player (studio runtime) in
addition to window.__hf and creates a bridge automatically. This lets
the renderer work with studio preview URLs directly.
- Studio useBrowserRender hook: force snapdom frame source and provide
explicit worker URL for Vite dev compatibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
drawElementImage: pixel-perfect capture via html-in-canvas API using
<canvas layoutsubtree> with iframe child. Achieves 42 dB PSNR vs 30 dB
for SnapDOM. Requires Chrome --enable-blink-features=CanvasDrawElement.
Tab-capture: experimental getDisplayMedia-based capture.
Also adds regression parity scripts for compiling test compositions and
comparing PSNR against golden reference renders.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Capture doc.body instead of doc.documentElement in SnapDOM to fix 75% viewport mismatch
- Make audio track conditional (hasAudio flag) to prevent corrupt empty audio tracks
- Add publishConfig for npm publishing
- Wire ProgressTracker for estimatedTimeRemaining and captureRate in progress callbacks
- Recompute video overlay canvas position per-frame for GSAP-animated elements
- Add AbortSignal to captureAll for cancel propagation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace requestAnimationFrame with setTimeout in __hf protocol polling
(rAF doesn't fire for off-screen iframes)
- Add workerUrl option to EncoderOptions and RenderConfig for configuring
the encoding worker URL (needed for bundled deployments)
- Add QA test page with self-contained CSS-animated composition
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix audio format: replace interleaveChannels with concatPlanarChannels
- Fix MIME type: derive video/mp4 vs video/webm from outputFormat in finalize
- Add isSupported() guard at top of render() for early failure
- Fix durationMs to report wall-clock render time (totalMs)
- Fix audio filter to use strict equality (hasAudio === true)
- Remove unused progress type from WorkerOutMessage union
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements mixAudio() with TDD: decodes audio sources, applies volume/timing
via GainNode, and renders a mixed PCM AudioBuffer using OfflineAudioContext.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the encoding pipeline for @hyperframes/renderer: worker message
protocol types, Web Worker entry point (VideoEncoder + MediaBunny muxer),
and the main-thread Encoder class that manages the worker lifecycle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements SnapdomFrameSource class that loads compositions in hidden iframes,
seeks via window.__hf.seek(), and captures DOM frames using @zumer/snapdom.
Adds jsdom dev dependency for the test environment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>