Skip to content

check: a media-metadata rebind rewinds scene timelines to 0 between a seek and its read, so findings change between identical runs #7706

Open

Description

@wonjangcloud9

Context: I've been using HyperFrames every day for the past two weeks to make Korean Shorts and long-form videos, and it's been great to work with. This is one of a batch of things I ran into along the way (most came as small PRs from me today) — sorry for the pile-up, and thanks for your patience.

Describe the bug

On projects built from data-composition-src scenes plus <audio>/<video> clips, hyperframes check --no-cache gives different findings on every run of an unchanged project, and some of them describe the t=0 frame instead of the sampled time. Two independent projects showed it from different sides:

Counts drift. A 37 s Short (nine scenes, a voice-over <audio> per scene), six back-to-back runs of 0.9.43:

run layout (info) contrast (warning) craft findings
1 6 1 10
2 9 0 10
3 9 1 11
4 5 1 10
5 5 1 10
6 8 0 10

The flapping container_overflows are each scene's Ken Burns wrapper, present when the sample sees it mid-zoom and absent when it sees scale 1. In another project a line drops out of one 8 fps craft sample, so line_unread measures 1.21 s instead of 2.10 s.

False contrast against the t=0 frame. Dark text on a light paper layer revealed by a 0.55 s clipPath tween was reported as

contrast_aa_failure #t2a  Contrast is 1.05:1; WCAG AA requires 3:1.  fg rgb(21,20,16)  bg rgb(13,13,14)

rgb(13,13,14) is the root's dark ground, which is what that spot shows at t=0, before the paper is revealed; a snapshot at the same time shows the text on paper. The background logic is fine (solidBackdrop returns null because the paper paints the region); the screenshot is of the wrong time. 7 such findings appeared in 5 of 15 runs, at different sample times. The number of text checks also varies (checked 14 / 20 / 23 / 24 / 39), because samples whose text is at opacity 0 at t=0 are silently skipped.

Minimal repro (main affa03b6, CLI 0.9.43)

  • index.html: root data-composition-id="main" with an empty window.__timelines.main = gsap.timeline({ paused: true }), one host <div data-composition-id="scene" data-composition-src="compositions/scene.html" data-start="0" data-duration="12">, and six <audio class="clip"> SFX (0.6 s sine WAVs) on track 6.
  • compositions/scene.html: dark #root, a full-frame light #paper revealed by tl.fromTo("#paper", { clipPath: "inset(100% 0% 0% 0%)" }, { clipPath: "inset(0% 0% 0% 0%)", duration: 0.55 }, 0), and two dark text lines fading in at 0.6 s and 3 s.

CI=1 hyperframes check --no-cache --json (CI=1 so no warm check process is reused):

variant runs with false #t1/#t2 1.05:1 (bg rgb(13,13,14))
with the six <audio> 4 of 6
same project, <audio> removed 0 of 5

Cause

(Line numbers at affa03b6.) Instrumenting the check page — wrapping the scene timelines' methods and dumping every window.__timelines[*].time() around each sample — shows the page clock (__player.getTime()) stays right while every scene timeline is put back at 0 between renderSeek and the read:

renderSeek [35.357]
s01.render [2.758]                            ← seekSceneTimeline puts the scene at its local time
s01.totalTime [0, 2.758]  at Timeline.paused  ← ensureChildCandidatesActive ← resolveRootTimelineFromDocument ← metadata debounce timer
s01.render [0]                                ← the scene is back at 0 s; check reads the frame now
  • Every <audio>/<video> registers scheduleMetadataDurationHydration (packages/core/src/runtime/init.ts:2648) on loadedmetadata and durationchange (:2803-2804). After METADATA_REBIND_DEBOUNCE_MS (100 ms, :875) it calls resolveRootTimelineFromDocument() (:2672) before deciding whether to rebind. On a check page those events keep arriving during the sweep, as clips near the seek position load.
  • resolveRootTimelineFromDocument() is not read-only: ensureChildCandidatesActive (:1919, paused(false) at :1928) nests and un-pauses the scene timelines, so GSAP renders each at its parent's playhead. Under renderSeek the scenes are driven standalone (seekStandaloneRegisteredTimelines, :5714) while the root stays at 0, so the visible frame jumps to t=0 — even when the timer then returns without rebinding.
  • The timer is skipped only when isExportRenderDrivingFrames() (:1428: renderCaptureSeekStarted && window.__HF_EXPORT_RENDER_SEEK_CONFIG != null); its comment names exactly this hazard for renders (HF#2550). check drives frames with the same renderSeek but sets no __HF_EXPORT_RENDER_SEEK_CONFIG, so it isn't covered. (init.test.ts:4196 keeps the rebind on after renderSeek without that config on purpose, for Studio's static-seek preview.)
  • The craft sweep uses DENSE_GEOMETRY_SEEK_OPTIONS (no settle) and reads straight after the seek, so whether the timer lands in between depends on when audio happens to load.

Verification

With a debug-only change making the timer return early once renderCaptureSeekStarted is set (not proposed as the fix), the 37 s Short gave identical results in 6/6 runs with one Chrome and 4/4 with the helper Chrome (layout 9 info, contrast 0, craft 10), and the second project gave 4/4 identical craft results.

Expected behavior

A seek and the read right after it describe the same time, so the same project and options give the same findings on every run.

Possible directions

  1. Let check mark its page as frame-driven, like the producer, so everything gated on isExportRenderDrivingFrames treats it as a render — with a marker only check and render set, since Studio's static-seek adapter must keep the self-correction.
  2. Split the probing part of resolveRootTimelineFromDocument from the part that nests and un-pauses scenes, so a rebind that isn't applied changes nothing; re-seek to state.currentTime after one that is.
  3. When the timer runs on a renderSeek-driven page, put back the frame renderSeek set before returning (e.g. seekTimelineAndAdapters(state.currentTime) in the same task).

Happy to send a PR for whichever direction you prefer.

Environment

hyperframes 0.9.43 (affa03b6), macOS arm64, chrome-headless-shell 152, Node 22.16.

Activity

  1. added
    bugSomething isn't working
    triage/needs-designEvidence is adequate; desired behavior or scope needs a maintainer decision.
    difficulty/hardDeep runtime, cross-system, compatibility, security, or validation complexity.
    on Nov 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdifficulty/hardDeep runtime, cross-system, compatibility, security, or validation complexity.triage/needs-designEvidence is adequate; desired behavior or scope needs a maintainer decision.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions