Repository navigation
fix(core): preview stops at the film's end when a scene loops forever - #5320
Merged
Merged
Conversation
Contributor
Edit accuracy: accurate 2059 (base branch 2059), smooth 1602 of thoseThe gate passes. Quarantined, measured but not gated (0) |
miguel-heygen
marked this pull request as ready for review
October 9, 2026 08:53
somanshreddy
approved these changes
Oct 9, 2026
somanshreddy
left a comment
Contributor
There was a problem hiding this comment.
Approving at efb3e664. I found no blockers.
- The fix holds. A loop-inflated root timeline (≥ 7200 s) now gives way to a floor or fallback, and keeps its length when there's neither. That's the same rule as the clip list's
timelineLooksLoopInflatedintimeline.ts. Every runtime caller ofgetSafeTimelineDurationSecondspasses a fallback of 0, so in the runtime only the floor half matters. - Moving the constant is safe.
compositionLength.tsimports onlycompositionDimension,domRealm,playbackRate,startResolverandstate, and none of those import./timeline, so the newtimeline.ts → compositionLengthimport adds no cycle. Studio'ssanitizeDurationSecondsnow reads the same constant through the publishedruntime/composition-lengthentry. - An endless floor stays endless, as before (
max(1e10, ∞)was already ∞). That matches the limit stated in the PR. - Tests: core
compositionLength,init.compositionLengthandtimelinepass: 105/105. - Mutants: 4 of 5 caught (revert the fix,
>=→>, drop the floor-or-fallback guard, drop the fallback fromfloorOrFallback). - Survivor (low): in the no-timeline branch,
seconds = floorOrFallback→seconds = floorisn't caught. No test has a floor below a larger fallback with no timeline. It can't affect the runtime, since every runtime caller passes a fallback of 0, but the published entry exposesfallback. A one-line case would pin it. - Nit: the doc comment on
LOOP_INFLATED_TIMELINE_SECONDS(GSAP's 1e10 forrepeat: -1, and Studio rejecting the same length) was dropped in the move. Worth keeping it next to the constant. - Merge and CI: merges cleanly with current main. No required check is red; Typecheck, Build and Render on windows were still running when I approved.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes for a user
When a scene has a GSAP tween on
repeat: -1and the root declares nodata-duration, Studio preview played past the end of the film. The counter read00:08 / 00:06, the picture went blank, the button stayed on Pause and Play could not replay the film. Now preview stops at the last frame like any other film, and Play starts again from 0.Render was already right (6.0 s output on the same fixture). Only preview was wrong.
Why
resolveCompositionLengthSecondstakes the longest of the timeline, the floors (sub-composition ends, media) and the fallback. GSAP reports about 1e10 s for a timeline that holds an infinite repeat, so the transport clock's end was 1e10 s. Studio's player paused at the real 6 s, but the runtime kept playing, so the two disagreed and the preview showed time past the end.timeline.tsalready treats a root timeline ofLOOP_INFLATED_TIMELINE_SECONDS(7200 s) or longer as loop-inflated and uses the finite window instead. The clock length now follows the same rule: a loop-inflated timeline gives way to a floor or fallback when one exists. With neither, it keeps the timeline length, as before.hyperframes lintalready flagsrepeat: -1without a rootdata-duration(gsap_infinite_repeat). This change makes preview agree with render when the author leaves it in.7200 s is also the length Studio's player rejects as loop-inflated (
sanitizeDurationSeconds); Studio now reads the sameLOOP_INFLATED_TIMELINE_SECONDSinstead of its own literal, so the two cannot drift. The constant now lives incompositionLength.ts(timeline.tsandinit.tsimport it), so the publishedruntime/composition-lengthentry does not loadtimeline.ts.Limits
hyperframes lintalready reportsrepeat: -1without a rootdata-duration.data-duration, finite root animation that runs past the last floor is cut at the floor (a 20 s tween plus a looping tween and a 6 s scene gives 6 s). This is deliberate. The other option was the loop-free end (__hf.animationEnd, 20 s there). Cutting at the floor keeps one rule for the film's length: Studio's clip list already ends the film at the last floor, so the transport and the clip list cannot disagree. A second rule would only serve a film the linter already flags:gsap_infinite_repeattells the author to set a rootdata-durationwheneverrepeat: -1is used, and that declared length always wins.data-durationand a shorter floor, would now preview and render at the floor. A rootdata-durationavoids it.timeline.ts) and this one count slightly different floors (this one also counts adapter-inferred lengths).Tests
Standalone on purpose: one root in the runtime clock, no other open change touches it.
compositionLength.test.ts: a floor or fallback wins over an endless timeline; with neither the timeline length stays; the boundary sits at exactly 7200 s. Exit 0, 23 passed, three runs in a row.init.compositionLength.test.ts: the running runtime reports 6 s for a root timeline at GSAP's endless length with a 2 s + 4 s scene. Exit 0, 11 passed, three runs in a row.compositionLength.test.tscases;>=to>fails the boundary test (7200 instead of 6).timeline.test.ts: exit 0, 71 passed. Studio:timelineSyncHydration.test.tsexit 0, 6 passed;useTimelineSyncCallbacks.test.tsexit 0, 17 passed; Studiotsc --noEmitclean. oxfmt, oxlint andtsc --noEmitfor core are clean.repeat: -1tween. Play from 4 s through the end, then press Play again.Before
Preview runs past the end (
00:08 / 00:06), blank picture, the button stays on Pause.After
Preview stops on the last frame at
00:06 / 00:06and Play replays from 0.