Skip to content

fix(core): music keeps playing while a preview video swaps to a new copy - #6937

Merged
miguel-heygen merged 2 commits into
mainfrom
fix/preview-swap-keeps-music
Nov 1, 2026
Merged

miguel-heygen merged 2 commits into
mainfrom
fix/preview-swap-keeps-music

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What

Music keeps playing when a preview video swaps to a new copy. The video shows a still of its last frame until the copy can draw. Before, the whole film stopped, music included, for about 200 ms.

Why

Each transport tick stops the film when a clip in view has no data (mediaNotReadyAt, then haltPlayback). A copy swap empties the video on purpose, so the swap itself counted as a stall. The clock and every playing track stopped, then restarted once the copy could draw.

How to reproduce: a film of muted video clips over one continuous audio clip, with a swap during play, such as a change of preview quality. A level meter on the audio element reads silence at the swap.

Related work

#6922 added the still that stands in during a swap; this PR lets the film rely on it. A swap of a video that has no frame to show still waits, as before.

How

  • stillStandsIn (in proxySrc.ts): true while a video shows its held still, for up to 2 s. mediaNotReadyAt skips such a video, so the film plays on.
  • After 2 s the copy counts as a real stall and the film waits with Buffering, as it does for any clip with no data.
  • heldPosters now also keeps when the hold began.

Known limit: the copy lands slightly behind the film (0.16 s in the run below), since the clock kept moving while it loaded. I did not measure how quickly that closes before the next cut.

Test plan

  • Unit tests added: init.buffering.test.ts
    • The music plays on while a video shows a still across a copy swap.
    • A copy that never lands stops standing in, and the film waits for it.
  • fails without the fix: packages/core/src/runtime/init.buffering.test.ts › the music plays on while a video shows a still across a copy swap
  • Removing the stillStandsIn check fails the first test; removing its time limit fails the second.
  • packages/core: init.buffering.test.ts and mediaProxy.test.ts passed 3 runs in a row; vitest run src/runtime 2702 passed; tsc --noEmit clean; oxlint and oxfmt on the changed files.
  • Manual testing performed: the desktop app on Linux with no GPU, 10 back-to-back muted HEVC clips over one mp3 (49.5 s, made from repo fixtures), the runtime built with and without this change, a quality step-up swapping every video during play, a level meter on the music.
    • Without the change, 2 runs: the music paused once at the swap in each, with 184 and 538 ms of silence.
    • With the change, 2 runs: no pause during play and no silence at the swap. The shown video held its still for about 180 ms, then drew.
    • Separate from this change: in 1 of the 2 runs with it, the music stayed at its first moments for about 2 s after Play and was re-seeked several times while the film ran, 30 s before any swap and with no still held. I have not found the cause.
  • Not run on macOS or Windows.
  • Comments follow CONTRIBUTING.md "Comments"

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 124 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 (0)

Unstable (1)

  • seqplainkeys-none-pct-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: APPROVE · 0 blocker · 1 important · 0 nits · head 50a927b0

The Comments ratchet fails, but it is not a required check. See the inline comment.

Scope

I read all 4 files. The only places heldPosters is set and released are in mediaProxy.ts, and both moved to the new {poster, since} shape. mediaNotReadyAt is the only stall gate, so stillStandsIn is the only bypass. A still is held only when the old copy had a frame, so a video with nothing to show still waits.

The fails without the fix: line holds: with !stillStandsIn(el) removed, the named test fails (1 failed, 5 passed); with it back, all 6 pass.

The ~0.16 s lag the PR lists as a known limit gets closed by rate steering, not a seek. The swap evicts the sync state, and 0.16 s is under the 0.5 s hard-sync threshold. That matters only for a swapped video that has its own audible track.

CI, CodeQL, captures, regression, Windows, Player perf and Safari audio are green. Player Safari perf was still running when I reviewed.

Comment thread packages/core/src/runtime/proxySrc.ts Outdated
return heldPosters.has(el) ? (heldPosters.get(el)?.poster ?? null) : el.getAttribute("poster");
}

// A swap takes well under a second; a copy still not drawing by now is a real stall.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

important: Comments fails here. proxySrc.ts goes from 14.3% comments (2/14) to 17.4% (4/23). Cutting this line is enough to pass, because the constant's name already says it.

@somanshreddy somanshreddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving at 5b91b744 on Miguel's re-stamp ask, after my own pass over the full diff. The only change since Genesis's approval at 50a927b0 is one deleted comment line in proxySrc.ts.

  • stillStandsIn: excludes a video showing its held still from mediaNotReadyAt for at most 2 s, measured from heldPosters.since. So a proxy swap no longer stops the clock and every playing track, while a copy that never lands still falls back to Buffering after 2 s. Both directions are pinned in init.buffering.test.ts: stand-in is ~1 s of play with the music unpaused, and expiry with since 60 s back gives a held clock and Buffering.
  • Type change: moving heldPosters from string | null to { poster, since } is applied at both readers: unheldPoster and the letGo restore in mediaProxy.ts.
  • Nits, non-blocking:
    • A re-swap while a still is already held keeps the first since (holdShownFrame returns early on heldPosters.has). So a panel that settles over more than 2 s of swaps reaches the Buffering fallback sooner than "2 s per swap" suggests.
    • The ~0.16 s lag matters only for a swapped video that plays its own audio, as stated in the PR.

— Somu

@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit 89a5953 Nov 1, 2026
98 checks passed
@miguel-heygen
miguel-heygen deleted the fix/preview-swap-keeps-music branch November 1, 2026 17:12
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