From b16f28fb74eee108a1a0eb94e7d83b2f5f11ba5b Mon Sep 17 00:00:00 2001 From: Santiago Puppo Date: Thu, 30 Jul 2026 14:43:33 -0300 Subject: [PATCH] feat(spf): Added autoplay support (#1880) --- .../dom/tests/track-load-triggers.test.ts | 31 +++++++++++++++++-- .../behaviors/dom/track-load-triggers.ts | 8 ++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/spf/src/playback/behaviors/dom/tests/track-load-triggers.test.ts b/packages/spf/src/playback/behaviors/dom/tests/track-load-triggers.test.ts index 043a5272..3b74a1bd 100644 --- a/packages/spf/src/playback/behaviors/dom/tests/track-load-triggers.test.ts +++ b/packages/spf/src/playback/behaviors/dom/tests/track-load-triggers.test.ts @@ -21,9 +21,18 @@ function setupTrackLoadTriggers(initialState: LoadTriggersState = {}, initialCon return { state, context, reactor }; } -/** Creates a video element with controllable `paused` and `seeking` state. */ -function makeMediaElement({ paused = true, seeking = false }: { paused?: boolean; seeking?: boolean } = {}) { +/** Creates a video element with controllable `paused`, `seeking`, and `autoplay` state. */ +function makeMediaElement({ + paused = true, + seeking = false, + autoplay = false, +}: { + paused?: boolean; + seeking?: boolean; + autoplay?: boolean; +} = {}) { const el = document.createElement('video'); + el.autoplay = autoplay; let pausedFlag = paused; let seekingFlag = seeking; Object.defineProperty(el, 'paused', { get: () => pausedFlag, configurable: true }); @@ -104,6 +113,24 @@ describe('trackLoadTriggers', () => { reactor.destroy(); }); + it('sets loadActivated to true immediately if element has autoplay on entry', async () => { + const { el } = makeMediaElement({ autoplay: true }); + const addEventListenerSpy = vi.spyOn(el, 'addEventListener'); + const { state, reactor } = setupTrackLoadTriggers( + { presentation: { url: 'http://example.com/stream.m3u8' } }, + { mediaElement: el } + ); + + await flush(); + + // Autoplay commits to loading up front (it can't wait for a `play` event + // that preload='none' would never let fire), so it activates on entry and + // never attaches play/seeking listeners. + expect(state.loadActivated.get()).toBe(true); + expect(addEventListenerSpy.mock.calls.some(([type]) => type === 'play' || type === 'seeking')).toBe(false); + reactor.destroy(); + }); + it('preserves pre-existing true write (adapter wrote before setup)', async () => { const { el } = makeMediaElement(); const addEventListenerSpy = vi.spyOn(el, 'addEventListener'); diff --git a/packages/spf/src/playback/behaviors/dom/track-load-triggers.ts b/packages/spf/src/playback/behaviors/dom/track-load-triggers.ts index 67ee7274..69d3a279 100644 --- a/packages/spf/src/playback/behaviors/dom/track-load-triggers.ts +++ b/packages/spf/src/playback/behaviors/dom/track-load-triggers.ts @@ -66,9 +66,9 @@ function deriveState( * * Writes `state.loadActivated = true` the first time a `play` or `seeking` * event fires on the attached media element for the current source — or - * immediately on entry if the element is already in such a state - * (`!el.paused` or `el.seeking`), mirroring autoplay / native-controls / - * direct-DOM-`play()` scenarios. + * immediately on entry if the element is already committed to loading + * (`el.autoplay`, `!el.paused`, or `el.seeking`), covering autoplay, + * native-controls, and direct-DOM-`play()` scenarios. * * Sticky-true *within a source identity*: subsequent play/pause/seek * cycles don't flip back. Source identity = (mediaElement, presentation @@ -125,7 +125,7 @@ function trackLoadTriggersSetup({ const el = context.mediaElement.get()!; const setLoadActivated = () => state.loadActivated.set(true); - if (!el.paused || el.seeking) { + if (el.autoplay || !el.paused || el.seeking) { setLoadActivated(); return; }