fix(runtime): guard deferred play with WeakSet to prevent load() spam

Address code review feedback: syncRuntimeMedia runs every tick, so
the deferred play path was calling load() and adding canplay listeners
on every frame — aborting and restarting the fetch repeatedly.

Fixes:
- Track pending elements in a WeakSet, skip if already deferred
- Listen for error event alongside canplay to clean up on failure
- Remove redundant manual removeEventListener (once: true handles it)
- Add tests: load() called, canplay callback fires, no re-registration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Miguel Ángel
2026-04-09 23:11:04 +02:00
co-authored by Claude Opus 4.6
parent f438f48268
commit 58f14b9807
2 changed files with 39 additions and 9 deletions
+23 -1
View File
@@ -178,15 +178,37 @@ describe("syncRuntimeMedia", () => {
expect(clip.el.play).toHaveBeenCalled();
});
it("defers play on unbuffered media until canplay fires", () => {
it("defers play on unbuffered media and calls load()", () => {
const clip = createMockClip({ start: 0, end: 10 });
Object.defineProperty(clip.el, "readyState", { value: 0, writable: true });
const loadSpy = vi.spyOn(clip.el, "load").mockImplementation(() => {});
const addEventSpy = vi.spyOn(clip.el, "addEventListener");
syncRuntimeMedia({ clips: [clip], timeSeconds: 5, playing: true, playbackRate: 1 });
expect(clip.el.play).not.toHaveBeenCalled();
expect(loadSpy).toHaveBeenCalledOnce();
expect(addEventSpy).toHaveBeenCalledWith("canplay", expect.any(Function), { once: true });
});
it("plays when canplay fires after deferred play", () => {
const clip = createMockClip({ start: 0, end: 10 });
Object.defineProperty(clip.el, "readyState", { value: 0, writable: true });
vi.spyOn(clip.el, "load").mockImplementation(() => {});
syncRuntimeMedia({ clips: [clip], timeSeconds: 5, playing: true, playbackRate: 1 });
expect(clip.el.play).not.toHaveBeenCalled();
clip.el.dispatchEvent(new Event("canplay"));
expect(clip.el.play).toHaveBeenCalled();
});
it("does not re-register listener on repeated ticks while unbuffered", () => {
const clip = createMockClip({ start: 0, end: 10 });
Object.defineProperty(clip.el, "readyState", { value: 0, writable: true });
const loadSpy = vi.spyOn(clip.el, "load").mockImplementation(() => {});
syncRuntimeMedia({ clips: [clip], timeSeconds: 5, playing: true, playbackRate: 1 });
syncRuntimeMedia({ clips: [clip], timeSeconds: 5.1, playing: true, playbackRate: 1 });
syncRuntimeMedia({ clips: [clip], timeSeconds: 5.2, playing: true, playbackRate: 1 });
expect(loadSpy).toHaveBeenCalledOnce();
});
it("pauses active clip when not playing", () => {
const clip = createMockClip({ start: 0, end: 10 });
Object.defineProperty(clip.el, "paused", { value: false, writable: true });
+16 -8
View File
@@ -66,6 +66,10 @@ export function refreshRuntimeMediaCache(params?: {
return { timedMediaEls: mediaEls, mediaClips, videoClips, maxMediaEnd };
}
// Elements with a pending deferred play — prevents re-calling load()/addEventListener
// on every tick while the media is still buffering.
const pendingPlay = new WeakSet<HTMLMediaElement>();
export function syncRuntimeMedia(params: {
clips: RuntimeMediaClip[];
timeSeconds: number;
@@ -100,18 +104,22 @@ export function syncRuntimeMedia(params: {
// ignore browser seek restrictions
}
}
if (params.playing && el.paused) {
if (params.playing && el.paused && !pendingPlay.has(el)) {
if (el.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
void el.play().catch(() => {});
} else {
// Media not yet buffered — start loading and play once ready.
pendingPlay.add(el);
if (el.preload !== "auto") el.preload = "auto";
const onCanPlay = () => {
el.removeEventListener("canplay", onCanPlay);
if (!el.paused) return; // already playing
void el.play().catch(() => {});
};
el.addEventListener("canplay", onCanPlay, { once: true });
el.addEventListener(
"canplay",
() => {
pendingPlay.delete(el);
if (!el.paused) return;
void el.play().catch(() => {});
},
{ once: true },
);
el.addEventListener("error", () => pendingPlay.delete(el), { once: true });
el.load();
}
} else if (!params.playing && !el.paused) {