Skip to content

fix(core): the runtime never reports a film's length as Infinity - #4372

Merged
miguel-heygen merged 2 commits into
mainfrom
fix/runtime-duration-not-loop
Sep 24, 2026
Merged

miguel-heygen merged 2 commits into
mainfrom
fix/runtime-duration-not-loop

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

The runtime's timeline message no longer reports a film's length as Infinity. A film whose animations end a little after its voiceover now reports its real length, and a genuinely looping timeline reports the finite length the runtime already chose for it.

The bug

collectRuntimeTimelinePayload flagged the root GSAP timeline as loop-inflated whenever it ran more than 1 s past the media or sub-composition window. With no data-duration on the root, the message then said durationSeconds: Infinity, durationInFrames: Infinity. That happened even though the runtime had already resolved the root's length to the media window.

A real film hit this: animations ending at 156 s, voiceover at 154.8 s, no root data-duration. The runtime player plays 156 s. Studio in same-origin mode recovers by reading the root's data-duration from the document. A host that sees only this message, such as @hyperframes/player (its message handler ignores a non-finite duration) or a preview in an opaque sandbox, has nothing to fall back on, and its total reads 00:00 while the film plays.

The fix, two commits

  1. Only endless-loop scale counts as loop-inflated: LOOP_INFLATED_TIMELINE_SECONDS = 7200, the length Studio's sanitizeDurationSeconds already rejects. GSAP reports 1e10 s for repeat: -1. Below it, the message uses the timeline, which is what the player's clock plays (max(timeline, media)).
  2. No more Infinity in the message. When a timeline is loop-inflated, the message carries the length the runtime resolved (the media window), instead of throwing it away.

What I measured

Check Result
timeline.test.ts 66 passed, exit 0
156 s timeline with a 154.8 s voiceover, on the old rule fails: expected Infinity to be 156
Endless loop (1e10) on commit 1 alone fails: expected Infinity to be 154.8
tsc --noEmit (packages/core, after build) / oxlint on the changed files exit 0 / 0 errors
Three real affected films, this branch's built runtime swapped into a sandboxed preview totals 02:36, 02:36, 01:42, all playing (were 00:00)

Review

Independent adversarial review, one round plus a delta on this head (delta: no BLOCKERs, no MAJORs):

Severity Finding Outcome
MAJOR 7200 s and above still emitted Infinity, so the same 00:00 Fixed at the source by commit 2
MAJOR A finite ambient loop (e.g. 300 s over 60 s of media) now reports 300 s, not Infinity Withdrawn in the delta: the player's clock plays 300 s there
MINOR 7200 duplicates Studio's literal Linked by comment; sharing one constant touches Studio and is a follow-up
MINOR Commit wording overstated agreement with the player for endless loops Commit 2's message makes no such claim

No reader of this message's duration was found outside the preview hosts (producer and thumbnails don't consume it).

What I did NOT exercise

  • A finite ambient-loop film walked in Studio (unit and probe level only).
  • With no media at all, a 1e10 timeline still reports 1e10, as before this change.
  • Studio's comment in useTimelineSyncCallbacks.ts saying the runtime can emit Infinity is now stale; its clamp stays correct for older runtimes. It's left for the Studio follow-up.

Before / After

A look-alike film with the same shape (animations end at 2:36, narration at 2:34.8, no root data-duration), played by <hyperframes-player controls sandbox-origin="opaque">. The film carries the runtime built from main (Before) or from this branch (After). Both are captured 4 s after Play. Before, the runtime reports Infinity and the player reads 0:04 / 0:00; after, it reports 156 and reads 0:04 / 2:36.

Before (main runtime): 0:04 / 0:00

After (this branch): 0:04 / 2:36

…length

The runtime treated any root timeline running more than 1 s past its media
as loop-inflated and, with no data-duration on the root, reported its length
as Infinity. Hosts that read only the runtime message, such as an
opaque-sandboxed preview, then showed a total of 00:00. Only a timeline at
endless-loop scale (7200 s, the length Studio already rejects) now counts
as loop-inflated.
When the root timeline looked loop-inflated and the root had no
data-duration, the timeline message said Infinity even though the runtime
had already chosen the media window as the root's length. Hosts that read
only this message (Studio's player, opaque-sandboxed previews) then showed a
total of 00:00. The message now carries the length the runtime resolved.

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at d8689fa2.

I checked the PR's claim that the player plays max(timeline, media) against the source. getSafeTimelineDurationSeconds (init.ts:1237) takes Math.max(timelineDuration, durationFloor, fallback). isUsableTimelineDuration has no upper bound, so below 7200 s the message now reports what the clock actually plays. That includes the finite ambient-loop case (300 s over 60 s of media).

Blast radius: timelineLooksLoopInflated has one reader, the preferredRootDuration choice. Changing it moves rootCompositionDuration, and so also timelineWindowEnd clip clamping and the root scene at :610. All of those now follow the same length the clock plays. An authored root data-duration still wins first, and with no finite floor a 1e10 timeline is unchanged, as the PR says.

Tests: timeline.test.ts passes 66/66 at this head (after building packages/parsers). I also broke the change on purpose three ways:

Change Result
Put back the > finiteWindowFloor + 1 rule the 156 s test fails
Put back the Infinity emission the endless-loop test fails
>= → > at 7200 66/66 still pass: the exact boundary isn't pinned

Notes, none blocking:

  • The third row: a test at exactly 7200 would lock this to the same boundary Studio's sanitizeDurationSeconds uses (value < 7200). Worth adding in the shared-constant follow-up you already listed.
  • For an endless loop with media, the message now says the media window (154.8) while the clock still runs to 1e10. That's strictly better than Infinity/0:00 for both hosts, but the message and the clock still disagree in that case. Your "did not exercise" list covers the no-media case, not this one.

— Rames

@miguel-heygen
miguel-heygen merged commit c6913aa into main Sep 24, 2026
166 of 209 checks passed
@miguel-heygen
miguel-heygen deleted the fix/runtime-duration-not-loop branch September 24, 2026 00:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants