Summary
When a composition mounts data-composition-src sub-compositions over a full-length <video>, the runtime can re-run the montage after the video's loadedmetadata arrives (diagnostic timeline_rebind_after_media_metadata). If that happens after frame capture has started, every sub-composition is mounted a second time: the data-composition-id collision is uniquified to …-1 (+ data-hf-original-composition-id), the renamed instance's scoped GSAP selectors stop matching, and captured frames composite two phase-offset copies of the block. The same project renders correctly when the metadata wins the race — so the output is non-deterministic across environments.
Evidence — same project, same CLI, different environment
Identical project directory (index.html + sub-compositions + local mp4/wav), rendered with stock npx hyperframes@0.7.60 render, frame at t=14s inside a 3 s flowchart-vertical window:
| macOS arm64 native (~34 s render) |
Docker linux/amd64, same files (~5 min render) |
 |
 |
| ONE instance |
TWO phase-offset instances |
Removing the <video> (changing nothing else) renders a single clean instance in both environments — the media-metadata event is the trigger.
Mechanism (as observed via runtime diagnostics)
On loadedmetadata/durationchange the runtime compares the captured root-timeline duration g with the media duration floor h and, when V(h) && (!V(g) || h >= g + T), emits timeline_rebind_after_media_metadata and re-runs the montage:
- metadata before capture (fast env): the
!e.capturedTimeline early path re-binds before anything is mounted — harmless;
- metadata after capture starts (slow env — big DOM, large media, slow I/O): the guarded rebind re-mounts every
data-composition-src host mid-capture → duplicate instance → id uniquify breaks one instance's selectors → both get composited at different timeline phases.
Which blocks make it visible
The re-mount hits every sub-composition, but visibility depends on the block: a block whose internal timeline far exceeds its window and whose animation targets scoped-id selectors (e.g. registry flowchart-vertical: 12 s timeline in a 3 s window) shows two drifting copies; blocks with timeline ≈ window duplicate "invisibly" (the twin overlaps the original). Light demo projects don't reproduce at all — metadata always beats capture — which makes this easy to miss in small tests.
Expected
Render output should be deterministic regardless of media-load timing. Candidate directions: wait for all media metadata before starting capture; freeze the montage once capture begins; or make the post-metadata rebind idempotent (skip mounting a data-composition-src host whose composition is already materialized).
Environment
hyperframes@0.7.60, mp4 render, captureMode beginframe in both environments
- Reproduces deterministically in the slow environment (10/10 renders), never natively on the same machine
Full public repro: https://github.com/jurczykpawel/hyperframes-double-mount-repro (same composition with generated media — reproduces identically). Happy to test candidate fixes and report back.
Summary
When a composition mounts
data-composition-srcsub-compositions over a full-length<video>, the runtime can re-run the montage after the video'sloadedmetadataarrives (diagnostictimeline_rebind_after_media_metadata). If that happens after frame capture has started, every sub-composition is mounted a second time: thedata-composition-idcollision is uniquified to…-1(+data-hf-original-composition-id), the renamed instance's scoped GSAP selectors stop matching, and captured frames composite two phase-offset copies of the block. The same project renders correctly when the metadata wins the race — so the output is non-deterministic across environments.Evidence — same project, same CLI, different environment
Identical project directory (index.html + sub-compositions + local mp4/wav), rendered with stock
npx hyperframes@0.7.60 render, frame at t=14s inside a 3 sflowchart-verticalwindow:Removing the
<video>(changing nothing else) renders a single clean instance in both environments — the media-metadata event is the trigger.Mechanism (as observed via runtime diagnostics)
On
loadedmetadata/durationchangethe runtime compares the captured root-timeline durationgwith the media duration floorhand, whenV(h) && (!V(g) || h >= g + T), emitstimeline_rebind_after_media_metadataand re-runs the montage:!e.capturedTimelineearly path re-binds before anything is mounted — harmless;data-composition-srchost mid-capture → duplicate instance → id uniquify breaks one instance's selectors → both get composited at different timeline phases.Which blocks make it visible
The re-mount hits every sub-composition, but visibility depends on the block: a block whose internal timeline far exceeds its window and whose animation targets scoped-id selectors (e.g. registry
flowchart-vertical: 12 s timeline in a 3 s window) shows two drifting copies; blocks with timeline ≈ window duplicate "invisibly" (the twin overlaps the original). Light demo projects don't reproduce at all — metadata always beats capture — which makes this easy to miss in small tests.Expected
Render output should be deterministic regardless of media-load timing. Candidate directions: wait for all media metadata before starting capture; freeze the montage once capture begins; or make the post-metadata rebind idempotent (skip mounting a
data-composition-srchost whose composition is already materialized).Environment
hyperframes@0.7.60, mp4 render, captureModebeginframein both environmentsFull public repro: https://github.com/jurczykpawel/hyperframes-double-mount-repro (same composition with generated media — reproduces identically). Happy to test candidate fixes and report back.