Skip to content

fix(core): a looping preview keeps playing when a scene reloads during a hold for data - #7659

Merged
terencecho merged 2 commits into
mainfrom
fix/loop-resume-after-buffer
Nov 13, 2026
Merged

terencecho merged 2 commits into
mainfrom
fix/loop-resume-after-buffer

Conversation

@terencecho

Copy link
Copy Markdown
Contributor

With loop turned on, a long film in the Studio preview now loops indefinitely. Before this change it wrapped back once, played a second time, then stopped at the start with the playhead frozen at 0:00 while the bar still showed Pause.

What you'll see

  • Turn on loop and play a film of a minute or more that opens on video and nested scenes: it keeps wrapping on every pass.
  • Short films looped fine before and still do.

Why

The Studio preview unloads scenes that are far from the playhead. In a long film, the opening scene gets unloaded during a full pass. At the next wrap back to the start:

  1. The runtime plays from 0. Its video and audio are still seeking, so it holds playback for data (bufferingFor), which is normal.
  2. The opening scene finishes reloading and re-seeks to the current time with keepPlaying: true.
  3. seekWhenReady and applySeek decided "playing" from clock.isPlaying() alone, and the clock is stopped during a hold for data. So the seek took the paused path: it cleared bufferingFor and landed as a paused seek.
  4. Nothing resumed the film. Studio's player loop still believed it was playing, so it never called play() again.

The first wrap usually works because the opening scene is still loaded then. That's why the loop stopped "after one loop" and only on long films.

How

A seek with keepPlaying now counts a hold for data as playing (clock.isPlaying() || bufferingFor.length > 0), in both seekWhenReady and applySeek. The seek then resumes through transport.play(). If the media still isn't ready, the next tick holds again, and it resumes as usual when the data arrives.

Test plan

  • Unit test added: init.buffering.test.ts, "a seek that keeps playing during a hold for data plays on once the track is ready". It fails on main (the film stays at 0) and passes with the fix.
  • vitest run src/runtime in packages/core: 137 files, 3,179 tests pass.
  • Manual: a 117-second film with an opening avatar video, music and nine nested scenes, played with loop at 1x. Before the fix it wrapped once, played the full film again, then froze at 0. With the fix it wrapped three times and was still playing when the run ended.
  • Comments follow CONTRIBUTING.md "Comments"

🤖 Generated with Claude Code

…g a hold for data

A Studio preview unloads scenes far from the playhead. On a long film, the
loop's wrap back to the start lands while the first scene is reloading: the
film holds for its video, then the scene swap re-seeks with keepPlaying. The
seek read the held film as paused, dropped the hold and parked the film at the
start, so the loop stopped after its second pass.

A seek that keeps playing now counts a hold for data as playing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 103 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

@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.

Reviewed at a0a715a9d15df3b653bdd8b300c22d9cf423f05d.

Need: yes, preserve playback through a buffer-held scene reload · Size: ok · Reuse: existing buffering state and transport.play() · Simpler: already minimal · Structure: fine (checked seekWhenReady/applySeek and transport; no split)
Risk: low — two seek decisions now preserve the existing logical playback state.

No blocking findings. Both seek decisions need the same definition of playing: a stopped clock can represent a buffer hold. Explicit pause still clears that intent, and paused keepPlaying seeks remain paused.

Validation: frozen install and full build passed; vitest run src/runtime --maxWorkers 2 passed all 137 files / 3,179 tests. Four additional fixture probes passed: pause before/after a held keepPlaying seek, four successive buffer-held seeks, and seeking away from a starved clip. Reverting the fix makes the new regression test fail. A routing-only revert also fails; reverting only the applySeek predicate still passes, so the test does not isolate that predicate. Changed-file lint and formatting passed. I did not independently repeat the reported 117-second Chrome walkthrough.

Nit — packages/core/src/runtime/init.buffering.test.ts:286–287: remove the historical “used to stop” clause, retaining the live invariant. The comment-citations check flags that clause; it does not affect runtime correctness.

Verdict: APPROVE
Reasoning: The fix preserves playback intent in both relevant seek paths and retains explicit pause behavior. The regression test reproduces the failure when the fix is reverted.

— Rames

@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.

Reviewed at 756a61a35d69d5d9256a2c3373fe07459cc9fa5b, including the delta from a0a715a9d15df3b653bdd8b300c22d9cf423f05d.

Need: yes, resolve the comment nit while retaining the loop fix · Size: ok · Reuse: playingOrHeldForData shared by both seek decisions · Simpler: already minimal · Structure: fine (checked both helper call sites; no split)
Risk: low — behavior-neutral predicate extraction and comment cleanup.

My previous comment nit is resolved. Mechanically inlining the new helper and restoring the deleted comment produces a byte-identical init.ts to the reviewed revision; the test differs only in comment wording. The closure reads the live clock and buffering array at each call.

Frozen install, changed-file lint/format, comment citations and comment ratchet pass at this head. I did not rerun the runtime suite: the prior 3,179-test run and revert probes were at a0a715a9d, and the executable behavior is unchanged by this delta.

Verdict: APPROVE
Reasoning: No new blockers; the requested comment cleanup is complete and both seek paths retain the reviewed buffer-hold behavior.

— Rames

@terencecho
terencecho enabled auto-merge November 13, 2026 19:01
@terencecho
terencecho added this pull request to the merge queue Nov 13, 2026
Merged via the queue into main with commit b8f630c Nov 13, 2026
149 of 151 checks passed
@terencecho
terencecho deleted the fix/loop-resume-after-buffer branch November 13, 2026 19:38
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