Skip to content

bug(cli): snapshot can capture stale/wrong frames in multi-composition projects #1047

@smartLanny

Description

@smartLanny

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:

  1. Wait for fonts:

    await document.fonts.ready
  2. 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
  3. Prefer render-owned seeking:

    await window.__player.renderSeek(t)

    falling back to window.__player.seek(t) only if renderSeek is unavailable.

  4. Wait for a couple of animation frames after seeking before screenshotting.

  5. Optionally verify window.__player.getTime() is close to the requested timestamp.

  6. 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:

  1. Open the composition in a browser.
  2. Wait for fonts, window.__playerReady, window.__player.renderSeek, and all child composition timelines.
  3. Call await window.__player.renderSeek(t).
  4. Wait 2 RAFs.
  5. Capture page.screenshot().

That keeps the speed advantage of snapshot while using the same timeline seek path as render.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions