bug(cli): snapshot can capture stale/wrong frames in multi-composition projects
Summary
hyperframes snapshot can produce screenshots that do not match Studio capture or hyperframes render output in a multi-composition project.
In the affected project:
- Studio preview looks correct.
- Studio capture looks correct.
hyperframes render followed by ffmpeg -ss ... -frames:v 1 produces the expected frames.
hyperframes snapshot sometimes captures background-only frames, stale/cached states, wrong scenes, or repeated frozen frames.
This makes snapshot unreliable for visual review even though render output is correct.
Environment
hyperframes: 0.6.38
- Invoked with:
npx --yes hyperframes@latest
- Project shape: one top-level
index.html with multiple clips/sub-compositions loaded through data-composition-src, each with GSAP-driven timelines.
- CLI flags tested:
--no-cache, --timeout 10000
Reproduction
From the project root:
rm -rf snapshots && \
npx --yes hyperframes@latest --no-cache snapshot . \
--at 0,1,2,3,4,5 \
--timeout 10000 && \
ls -lhT snapshots/ && \
shasum snapshots/*.png
Observed in repeated debugging runs:
0s often captured a background-only frame.
1s captured content.
2s through 5s sometimes produced identical hashes, suggesting the timeline stopped advancing or the same visual state was reused.
Another real review set:
rm -rf snapshots && \
npx --yes hyperframes@latest --no-cache snapshot . \
--at 3.2,10.0,18.2,25.2,33.0,36.8,54.0,60.0 \
--timeout 10000 && \
ls -lhT snapshots/ && \
shasum snapshots/*.png
Observed examples:
- Some timestamps captured the wrong scene.
- Later timestamps sometimes became background-only.
- Several background-only frames shared the same hash.
- Clearing browser cache and increasing
--timeout did not make this stable.
- Top-level
--no-cache improved behavior but did not fully eliminate wrong-time or frozen-frame captures.
The equivalent render-based review path was correct:
npx --yes hyperframes@latest render . \
--fps 2 \
--quality draft \
--workers 1 \
--output renders/debug-snapshot-vs-render-2fps.mp4
mkdir -p review-frames/debug-snapshot-vs-render
for t in 3.2 10.0 18.2 25.2 33.0 36.8 54.0 60.0; do
ffmpeg -y -ss "$t" \
-i renders/debug-snapshot-vs-render-2fps.mp4 \
-frames:v 1 \
"review-frames/debug-snapshot-vs-render/frame-at-${t}s.png"
done
Expected behavior
For any timestamp t, hyperframes snapshot --at t should match the same timestamp extracted from hyperframes render, modulo normal PNG/MP4 compression differences.
Snapshot should not capture before the player/sub-composition runtime is ready, and it should not use a seek path that differs from render's effective timeline state.
Suspected cause
While debugging the installed CLI bundle, the snapshot path appeared to be too permissive around readiness and seeking:
- The readiness check can proceed when
window.__timelines exists or window.__playerReady is truthy, even if the full player/sub-composition runtime is not ready.
- Waiting for sub-composition timelines can time out without failing the snapshot command, so the command may continue and capture an incomplete state.
- The fallback path manually seeks GSAP timelines. In a top-level project with multiple clips and nested
data-composition-src sub-compositions, that is not equivalent to render/player seek semantics because clip visibility, child composition activation, and local-time mapping can diverge.
- If the player is not fully ready, snapshot can therefore capture a stale state, a wrong clip, or only the background.
This looks related to the same area touched by previous runtime/seek fixes, but it does not appear to have an open issue:
Local mitigation that fixed the observed mismatch
A local CLI patch made snapshot behave like a targeted render-frame capture:
-
Wait for fonts:
await document.fonts.ready
-
Wait for a stricter runtime-ready condition:
window.__playerReady === true
window.__player exists
window.__player.renderSeek or window.__player.seek exists
- expected
data-composition-src child timelines are registered in window.__timelines
-
Prefer render-owned seeking:
await window.__player.renderSeek(t)
falling back to window.__player.seek(t) only if renderSeek is unavailable.
-
Wait for a couple of animation frames after seeking before screenshotting.
-
Optionally verify window.__player.getTime() is close to the requested timestamp.
-
If the runtime is not ready, fail with diagnostics instead of silently continuing and capturing a likely-wrong frame.
After this change, the same snapshot commands aligned with render-extracted frames by scene and animation state in the affected project.
Workaround
Until snapshot uses render/player seek semantics reliably, the safest visual-review workaround is:
npx --yes hyperframes@latest render . \
--fps 2 \
--quality draft \
--workers 1 \
--output renders/review-2fps.mp4
ffmpeg -ss "$t" -i renders/review-2fps.mp4 -frames:v 1 frame-at-${t}s.png
This is slower than snapshot, but it matches final render behavior.
An efficient workaround is to use Playwright/Puppeteer directly:
- Open the composition in a browser.
- Wait for fonts,
window.__playerReady, window.__player.renderSeek, and all child composition timelines.
- Call
await window.__player.renderSeek(t).
- Wait 2 RAFs.
- Capture
page.screenshot().
That keeps the speed advantage of snapshot while using the same timeline seek path as render.
bug(cli): snapshot can capture stale/wrong frames in multi-composition projects
Summary
hyperframes snapshotcan produce screenshots that do not match Studio capture orhyperframes renderoutput in a multi-composition project.In the affected project:
hyperframes renderfollowed byffmpeg -ss ... -frames:v 1produces the expected frames.hyperframes snapshotsometimes captures background-only frames, stale/cached states, wrong scenes, or repeated frozen frames.This makes
snapshotunreliable for visual review even though render output is correct.Environment
hyperframes:0.6.38npx --yes hyperframes@latestindex.htmlwith multiple clips/sub-compositions loaded throughdata-composition-src, each with GSAP-driven timelines.--no-cache,--timeout 10000Reproduction
From the project root:
Observed in repeated debugging runs:
0soften captured a background-only frame.1scaptured content.2sthrough5ssometimes produced identical hashes, suggesting the timeline stopped advancing or the same visual state was reused.Another real review set:
Observed examples:
--timeoutdid not make this stable.--no-cacheimproved behavior but did not fully eliminate wrong-time or frozen-frame captures.The equivalent render-based review path was correct:
Expected behavior
For any timestamp
t,hyperframes snapshot --at tshould match the same timestamp extracted fromhyperframes render, modulo normal PNG/MP4 compression differences.Snapshot should not capture before the player/sub-composition runtime is ready, and it should not use a seek path that differs from render's effective timeline state.
Suspected cause
While debugging the installed CLI bundle, the snapshot path appeared to be too permissive around readiness and seeking:
window.__timelinesexists orwindow.__playerReadyis truthy, even if the full player/sub-composition runtime is not ready.data-composition-srcsub-compositions, that is not equivalent to render/player seek semantics because clip visibility, child composition activation, and local-time mapping can diverge.This looks related to the same area touched by previous runtime/seek fixes, but it does not appear to have an open issue:
Local mitigation that fixed the observed mismatch
A local CLI patch made
snapshotbehave like a targeted render-frame capture:Wait for fonts:
Wait for a stricter runtime-ready condition:
window.__playerReady === truewindow.__playerexistswindow.__player.renderSeekorwindow.__player.seekexistsdata-composition-srcchild timelines are registered inwindow.__timelinesPrefer render-owned seeking:
falling back to
window.__player.seek(t)only ifrenderSeekis unavailable.Wait for a couple of animation frames after seeking before screenshotting.
Optionally verify
window.__player.getTime()is close to the requested timestamp.If the runtime is not ready, fail with diagnostics instead of silently continuing and capturing a likely-wrong frame.
After this change, the same snapshot commands aligned with render-extracted frames by scene and animation state in the affected project.
Workaround
Until
snapshotuses render/player seek semantics reliably, the safest visual-review workaround is:This is slower than snapshot, but it matches final render behavior.
An efficient workaround is to use Playwright/Puppeteer directly:
window.__playerReady,window.__player.renderSeek, and all child composition timelines.await window.__player.renderSeek(t).page.screenshot().That keeps the speed advantage of snapshot while using the same timeline seek path as render.