From 2546ed040aeee6e6492812e714f01ff23464e604 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sun, 1 Nov 2026 15:19:47 -0500 Subject: [PATCH] fix(core): only the clips the start wait gave up on run on without data --- .../core/src/runtime/init.buffering.test.ts | 17 ++++++++++++++ packages/core/src/runtime/init.ts | 22 ++++++++++--------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/core/src/runtime/init.buffering.test.ts b/packages/core/src/runtime/init.buffering.test.ts index e4d6c08295..870df4d661 100644 --- a/packages/core/src/runtime/init.buffering.test.ts +++ b/packages/core/src/runtime/init.buffering.test.ts @@ -148,6 +148,23 @@ it("after a start that ran on without data, the next clip without data holds aga expect(states.at(-1)).toEqual({ isPlaying: true, buffering: true }); }); +it("a clip due while another catches up from the start still holds, then plays with the first one still cold", () => { + const { voiceReady, states, playFor } = playPodcast(0.5, "audio", 1); + + playFor(0.45); + expect(states.at(-1)).toEqual({ isPlaying: true, buffering: false }); + playFor(0.45); + expect(states.at(-1)).toEqual({ isPlaying: true, buffering: true }); + const heldAt = window.__player!.getTime(); + playFor(0.3); + expect(window.__player!.getTime()).toBe(heldAt); + + voiceReady.value = 4; + playFor(0.3); + expect(states.at(-1)).toEqual({ isPlaying: true, buffering: false }); + expect(window.__player!.getTime()).toBeGreaterThan(heldAt); +}); + it("holds on the frame a slow tick skips into a clip with no data, after asking it to play", () => { const { voice, states, playFor, step } = playPodcast(0.5); diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index 7264883405..21c5bedf65 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -3447,7 +3447,7 @@ export function initSandboxRuntimeModular(): void { // Play waits this long for a clip with no data, then runs on and lets the clip catch up; mid-film it holds. const START_WAIT_MS = 300; let startWaitUntilMs: number | null = null; - let catchingUp = false; + let catchingUpFor: HTMLMediaElement[] = []; let resumingFromBuffering = false; // The one definition of "media the transport drives": timed itself, or hosted @@ -4785,7 +4785,7 @@ export function initSandboxRuntimeModular(): void { bufferingFor = []; if (!resumingFromBuffering) { startWaitUntilMs = performance.now() + START_WAIT_MS; - catchingUp = false; + catchingUpFor = []; } const tl = state.capturedTimeline; if (clock.isPlaying()) return; @@ -4822,7 +4822,7 @@ export function initSandboxRuntimeModular(): void { const wasBuffering = bufferingFor.length > 0; bufferingFor = []; startWaitUntilMs = null; - catchingUp = false; + catchingUpFor = []; if (clock.isPlaying()) haltPlayback(); else if (wasBuffering) postState(true); }, @@ -5674,7 +5674,7 @@ export function initSandboxRuntimeModular(): void { if (clock.hasAudioSource()) clock.detachAudioSource(); } else if ( source && - catchingUp && + catchingUpFor.includes(source.el) && source.el.readyState < HTMLMediaElement.HAVE_FUTURE_DATA ) { // Past the start wait the playhead runs on its own clock while this track catches up. @@ -5908,16 +5908,18 @@ export function initSandboxRuntimeModular(): void { const notReady = mediaNotReadyAt(state.currentTime); if (startWaitUntilMs !== null && performance.now() >= startWaitUntilMs) { startWaitUntilMs = null; - catchingUp = notReady.length > 0; + catchingUpFor = notReady; } - if (notReady.length === 0) catchingUp = false; - if (clock.isPlaying() && notReady.length > 0 && !catchingUp) { - bufferingFor = notReady; + catchingUpFor = catchingUpFor.filter((el) => notReady.includes(el)); + const mustWaitFor = notReady.filter((el) => !catchingUpFor.includes(el)); + if (clock.isPlaying() && mustWaitFor.length > 0) { + bufferingFor = mustWaitFor; haltPlayback(true); } else if ( bufferingFor.length > 0 && - (catchingUp || - !mediaNotReadyAt(state.currentTime, canResumeFrom).some((el) => bufferingFor.includes(el))) + !mediaNotReadyAt(state.currentTime, canResumeFrom) + .filter((el) => !catchingUpFor.includes(el)) + .some((el) => bufferingFor.includes(el)) ) { resumingFromBuffering = true; try {