diff --git a/packages/core/src/runtime/init.buffering.test.ts b/packages/core/src/runtime/init.buffering.test.ts index 84ee600405..25d1dc804c 100644 --- a/packages/core/src/runtime/init.buffering.test.ts +++ b/packages/core/src/runtime/init.buffering.test.ts @@ -227,6 +227,52 @@ it("a hold for data does not reload a later clip", () => { expect(load.mock.contexts.filter((el) => el === later).length).toBe(before); }); +function laterClipHoldingARequest() { + const later = document.getElementById("later") as HTMLVideoElement; + Object.defineProperties(later, { + readyState: { get: () => 1, configurable: true }, + networkState: { get: () => 2, configurable: true }, + }); + const srcWrites = new MutationObserver(() => {}); + srcWrites.observe(later, { attributeFilter: ["src"] }); + return { later, srcWrites }; +} + +it("a hold whose clip gets no data for 5 s frees the clips not due, whose open requests can fill the connection", () => { + const { voice, voiceReady, playFor } = playPodcast( + 0, + "video", + '', + ); + const { later, srcWrites } = laterClipHoldingARequest(); + + playFor(4); + expect(srcWrites.takeRecords()).toHaveLength(0); + playFor(1.5); + expect(srcWrites.takeRecords().length).toBeGreaterThan(0); + expect([later.preload, later.getAttribute("src")]).toEqual(["none", "/later.mp4"]); + expect(voice.getAttribute("src")).toBe("/voice.m4a"); + + voiceReady.value = 4; + playFor(1); + expect(window.__player!.getTime()).toBeGreaterThan(0.9); +}); + +it("a hold whose clip is still receiving data frees nothing", () => { + const { voiceReady, playFor } = playPodcast( + 0, + "video", + '', + ); + const { srcWrites } = laterClipHoldingARequest(); + + for (let s = 1; s <= 8; s++) { + voiceReady.bufferedTo = s * 0.05; + playFor(1); + } + expect(srcWrites.takeRecords()).toHaveLength(0); +}); + it("a film held for data reads as choppy, so the app can lower it", () => { const reports: Array<{ choppy: boolean; waitingShare: number; heldBy: unknown }> = []; const { voiceReady, playFor } = playPodcast(0, "video"); diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index 86eee3beea..2224823f04 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -6194,6 +6194,32 @@ export function initSandboxRuntimeModular(): void { ) .map(({ el }) => el); + const HELD_WITHOUT_PROGRESS_MS = 5000; + const loadProgress = (els: HTMLMediaElement[]) => + els + .map((el) => { + let seconds = 0; + for (let i = 0; i < el.buffered.length; i++) + seconds += el.buffered.end(i) - el.buffered.start(i); + return `${el.readyState}:${seconds}`; + }) + .join(); + let heldProgress = { held: bufferingFor, mark: "", sinceMs: 0 }; + const releaseMediaHoldingTheConnection = () => { + const nowMs = performance.now(); + const mark = loadProgress(bufferingFor); + if (heldProgress.held !== bufferingFor || heldProgress.mark !== mark) + heldProgress = { held: bufferingFor, mark, sinceMs: nowMs }; + if (nowMs - heldProgress.sinceMs < HELD_WITHOUT_PROGRESS_MS) return; + heldProgress.sinceMs = nowMs; + for (const { el, start, end } of resolveMediaClipIndex().clips) { + if (bufferingFor.includes(el) || isInClipWindow(state.currentTime, start, end)) continue; + if (el.readyState === 0 && el.networkState !== 2) continue; + el.preload = "none"; + withoutOwnTimingRecords(el, () => releaseMedia(el)); + } + }; + const transportTick = () => { if (state.tornDown || inTransportTick) return; inTransportTick = true; @@ -6381,6 +6407,8 @@ export function initSandboxRuntimeModular(): void { !mediaNotReadyAt(state.currentTime, canResumeFrom).some((el) => bufferingFor.includes(el)) ) { transport.play(); + } else if (bufferingFor.length > 0) { + releaseMediaHoldingTheConnection(); } postState(false); } finally {