Skip to content

1.7: MediaSource reset re-attaches over a src set by a third party on the attached media element (breaks IMA custom playback Ads on iOS) #8073

Description

@nochev

What version of HLS.js are you using?

1.7.3 (also 1.7.4-0.canary.12041 on the dev demo). Regression from 1.6.19; first bad pre-release is v1.7.0-alpha.1.

What browser (including version) are you using?

Reproduces in any browser. Observed on iPhone 17e Safari iOS 26 (ManagedMediaSource) and desktop Chrome (MediaSource).

What OS (including version) are you using?

iOS 26 (BrowserStack iPhone 17e), Windows 10

Test stream

https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8 (any stream)

Configuration

{} // defaults; the demo page config as-is

Additional player setup steps

The Google IMA SDK in iPhone "custom playback" mode (the default on iPhone unless disableCustomPlaybackForIOS10Plus is set) plays linear Ads in the content <video> element by replacing its src, then hands the element back when the Ad finishes. hls.js stays attached during the Ad; the player restores the content source afterwards. This has worked through 1.6.x.

The steps below simulate exactly what the SDK does (video.pause(); video.src = mp4; video.play()) on the demo page. A standalone page with full event/MediaSource logging and a 1.7.3 / 1.6.19 switch (stock builds from jsDelivr) is here:
https://github.com/nochev/hls.js/blob/repro/src-replaced-while-attached/demo/src-replaced-while-attached.html

Checklist

Steps to reproduce

  1. Open https://hlsjs.video-dev.org/demo/?src=https%3A%2F%2Ftest-streams.mux.dev%2Fx36xhzz%2Fx36xhzz.m3u8 and play for a few seconds.
  2. In the console, do what an Ad SDK in custom-playback mode does:
    const v = document.querySelector('video');
    v.pause();
    v.src = 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4';
    v.play().catch(e => console.log('play() rejected:', e.name, e.message));
  3. Watch the console and v.currentSrc.

Same result on https://hlsjs-dev.video-dev.org/demo (1.7.4-0.canary.12041). With 1.6.19 the MP4 plays.

Expected behaviour

Same as 1.6.19: hls.js logs "Media source closed", its appends start failing (bufferAppendError ... fatal, stopLoad), but it leaves the element alone. flower.mp4 loads (loadedmetadata, duration 5.011), plays and reaches ended; the app then restores the HLS source itself.

What actually happened?

Setting src on the attached element fires sourceclose. Since #7683 (51725fcc8, bfcache fix) _onMediaSourceClose treats that as a dead MediaSource and raises MEDIA_SOURCE_REQUIRES_RESET → recoverMediaError() → detach → re-attach. Re-attaching overwrites the MP4 src with a new MediaSource object URL, the pending play() is rejected with AbortError: The play() request was interrupted by a new load request, and HLS content resumes instead of the Ad. On the stable demo this takes ~10 ms; v.currentSrc ends up as a new blob: URL and duration is the HLS duration (634 s), not 5 s.

Two things make this unrecoverable from the application side:

  1. onErrorOut is registered in the Hls constructor (src/hls.ts#L351-L357), so it runs before any hls.on(Events.ERROR) listener the app adds. By the time the app sees MEDIA_SOURCE_REQUIRES_RESET, recoverMediaError() has already been called and ResetMediaSource is set in errorAction.flags (unit test on the repro branch: tests/unit/controller/error-controller.ts "calls recoverMediaError for MEDIA_SOURCE_REQUIRES_RESET before application ERROR listeners can intervene"). So errorAction.action = DoNothing / resolved = true from the app has no effect, contrary to the intent stated in Introduce MEDIA_SOURCE_REQUIRES_RESET error event (MediaSource closed while attached or ended on append error) #7699.
  2. hls.js already has a "src was changed by a third party - skip cleanup" check at detach (buffer-controller.ts ~L571-L583), but the mediaSrc getter prefers the <source> child over the src attribute. On iOS, ManagedMediaSource is attached via <source>, so the getter still returns the blob URL after IMA set video.src, the check misses, and cleanup removes the URL of Ad. (On desktop Chrome, where there is no <source> child, the detach check does fire, but the re-attach still overwrites the src.)

Where the behaviour comes from (at c3e5e8154)

Proposed fix

Skip the reset when the element's source was replaced by a third party, using the same "src changed" notion the detach cleanup already uses, and make mediaSrc honour HTML resource selection (a src attribute wins over <source> children):

master...nochev:hls.js:fix/skip-media-source-reset-on-third-party-src

  • _onMediaSourceClose: if mediaSrcChangedByThirdParty, warn and return (no MEDIA_SOURCE_REQUIRES_RESET).
  • append-error branch: same guard, so the subsequent failing appends degrade exactly like 1.6.x (bufferAppendError → fatal → stopLoad) instead of resetting.
  • mediaSrc getter: prefer media.src when set, otherwise the <source> child.
  • 3 unit tests in tests/unit/controller/buffer-controller.ts.

Verified with a build of that branch on the same iPhone (ManagedMediaSource), same steps:

[+36.131s] [video#content] src attribute set to https://.../flower.mp4 | paused=true ms=closed
[+36.325s] [hls.log] [buffer-controller]: Media source closed
[+36.325s] [hls.warn] [buffer-controller]: MediaSource closed after media|source.src was changed by a third party (blob:http://bs-local.com/307658e7-... > https://.../flower.mp4) - skipping reset
[+36.326s] [MediaSource#1] sourceclose
[+36.498s] [video#content] loadedmetadata currentSrc=https://.../flower.mp4 duration=5.011   <- the MP4
[+36.585s] [video#content] playing
[+36.770s] [hls.warn] [buffer-controller]: Failed 1/4 times to append segment in "video" sourceBuffer with error: The object is in an invalid state.
... (level switches, more bufferAppendError, same as 1.6.19)
[+37.995s] [hls.log] stopLoad
[+37.997s] [hlsError] bufferAppendError fatal=true error="The object is in an invalid state."
[+41.956s] [video#content] ended | t=5.01

No recoverMediaError(), no re-attach, no AbortError; the element is left to the third party exactly as in 1.6.x. Also verified in desktop Chrome (plain MediaSource), and in our production player (Video.js 8 + videojs-ima, IMA custom playback on iPhone) with VAST pre-, mid- and post-roll Ads: behaviour matches 1.6.19.

PR: #8074 (happy to adjust it, e.g. stop loading immediately instead of letting the appends fail, if you prefer).

Console output

--- hlsjs.video-dev.org/demo, 1.7.3, desktop Chrome, steps above (t0 = src set) ---
[+0.007s] recoverMediaError() called
[+0.009s] hlsMediaDetaching
[+0.011s] hlsMediaAttaching
[+0.011s] hlsError mediaSourceRequiresReset fatal=false action={"action":2,"flags":24,"nextAutoLevel":3,"resolved":true}
[+0.011s] play() rejected: AbortError: The play() request was interrupted by a new load request.
[+0.012s] video emptied
[+0.014s] video loadstart currentSrc=blob:https://hlsjs.video-dev.org/8dd37889-...
[+0.016s] hlsMediaAttached
[+0.167s] video loadedmetadata currentSrc=blob:https://hlsjs.video-dev.org/8dd37889-... duration=634.56   <- HLS content, not the MP4
[+0.460s] video playing

--- iPhone Safari, 1.7.3, repro page (paused= / ms= are video.paused and mediaSource.readyState at log time) ---
[+52.461s] [video#content] src attribute set to https://.../flower.mp4 | paused=true ms=closed
[+52.652s] [hls.log] [buffer-controller]: Media source closed
[+52.652s] [hls.warn] [buffer-controller]: Error: MediaSource closed while media attached - triggering recovery
[+52.653s] [hls.warn] [error-controller]: switching to level 3 after mediaSourceRequiresReset
[+52.654s] [hls] recoverMediaError() called
[+52.659s] [hlsMediaDetaching]
[+52.659s] [hlsMediaDetached]
[+52.659s] [hls.log] [buffer-controller]: created media source: ManagedMediaSource
[+52.659s] [hlsMediaAttaching] media=content transferred=false
[+52.660s] [hlsError] mediaSourceRequiresReset fatal=false errorAction={"action":2,"flags":24,"nextAutoLevel":3,"resolved":true}   <- app listener runs only now
[+52.660s] [content.play()] rejected: AbortError: The operation was aborted.
[+52.669s] [video#content] loadstart
[+52.672s] [hlsMediaAttached]
[+52.673s] [MediaSource#2] sourceopen
[+52.954s] [video#content] loadedmetadata currentSrc=blob:http://bs-local.com/a9862859-... duration=634.58   <- HLS content, not the MP4
[+53.240s] [video#content] playing | t=3.82

--- iPhone Safari, 1.6.19, same steps ---
[+15.495s] [video#content] src attribute set to https://.../flower.mp4 | paused=true ms=closed
[+15.692s] [hls.log] [buffer-controller]: Media source closed
[+15.692s] [MediaSource#1] sourceclose
[+15.700s] [video#content] loadstart
[+15.846s] [video#content] loadedmetadata currentSrc=https://.../flower.mp4 duration=5.011   <- the MP4
[+16.047s] [video#content] playing
[+16.144s] [hlsError] bufferAppendError fatal=false error="The object is in an invalid state."
... (level switches, more bufferAppendError)
[+17.001s] [hls.log] stopLoad
[+17.002s] [hlsError] bufferAppendError fatal=true error="The object is in an invalid state."
[+21.391s] [video#content] ended | t=5.01

Chrome media internals output

Activity

  1. added
    Needs TriageIf there is a suspected stream issue, apply this label to triage if it is something we should fix.
    on Oct 9, 2026
  2. robwalch commented on Oct 9, 2026

    @robwalch
    Collaborator

    I would expect integrations using IMA and HLS.js to detach the media element from HLS.js when the other library is using it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugNeeds TriageIf there is a suspected stream issue, apply this label to triage if it is something we should fix.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions