You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
The Google IMA SDK in iPhone "custom playback" mode (the default on iPhone unless disableCustomPlaybackForIOS10Plus is set) plays linear Adsin 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.
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:
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.
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.)
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):
_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
What version of HLS.js are you using?
1.7.3 (also
1.7.4-0.canary.12041on the dev demo). Regression from1.6.19; first bad pre-release isv1.7.0-alpha.1.What browser (including version) are you using?
Reproduces in any browser. Observed on
iPhone 17e Safari iOS 26(ManagedMediaSource) and desktopChrome(MediaSource).What OS (including version) are you using?
iOS 26 (
BrowserStackiPhone 17e), Windows 10Test stream
https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8 (any stream)
Configuration
Additional player setup steps
The
Google IMA SDKiniPhone"custom playback" mode (the default oniPhoneunlessdisableCustomPlaybackForIOS10Plusis set) plays linearAdsin the content<video>element by replacing itssrc, then hands the element back when theAdfinishes.hls.jsstays attached during theAd; the player restores the content source afterwards. This has worked through1.6.x.The steps below simulate exactly what the
SDKdoes (video.pause(); video.src = mp4; video.play()) on the demo page. A standalone page with fullevent/MediaSourcelogging and a1.7.3/1.6.19switch (stock builds fromjsDelivr) is here:https://github.com/nochev/hls.js/blob/repro/src-replaced-while-attached/demo/src-replaced-while-attached.html
Checklist
Steps to reproduce
AdSDK in custom-playback mode does:v.currentSrc.Same result on https://hlsjs-dev.video-dev.org/demo (
1.7.4-0.canary.12041). With1.6.19theMP4plays.Expected behaviour
Same as
1.6.19:hls.jslogs "Media source closed", its appends start failing (bufferAppendError... fatal,stopLoad), but it leaves the element alone.flower.mp4loads (loadedmetadata, duration 5.011), plays and reachesended; the app then restores theHLSsource itself.What actually happened?
Setting
srcon the attached element firessourceclose. Since #7683 (51725fcc8,bfcachefix)_onMediaSourceClosetreats that as a deadMediaSourceand raisesMEDIA_SOURCE_REQUIRES_RESET→recoverMediaError()→ detach → re-attach. Re-attaching overwrites the MP4 src with a newMediaSourceobjectURL, the pendingplay()is rejected withAbortError: The play() request was interrupted by a new load request, andHLScontent resumes instead of theAd. On the stable demo this takes ~10 ms;v.currentSrcends up as a newblob:URLanddurationis theHLSduration (634 s), not 5 s.Two things make this unrecoverable from the application side:
onErrorOutis registered in theHlsconstructor (src/hls.ts#L351-L357), so it runs before anyhls.on(Events.ERROR)listener the app adds. By the time the app seesMEDIA_SOURCE_REQUIRES_RESET,recoverMediaError()has already been called andResetMediaSourceis set inerrorAction.flags(unit test on the repro branch:tests/unit/controller/error-controller.ts"callsrecoverMediaErrorforMEDIA_SOURCE_REQUIRES_RESETbefore applicationERRORlisteners can intervene"). SoerrorAction.action = DoNothing/resolved = truefrom the app has no effect, contrary to the intent stated in IntroduceMEDIA_SOURCE_REQUIRES_RESETerror event (MediaSource closed while attached or ended on append error) #7699.hls.jsalready has a "src was changed by a third party - skip cleanup" check at detach (buffer-controller.ts~L571-L583), but themediaSrcgetter prefers the<source>child over thesrcattribute. OniOS,ManagedMediaSourceis attached via<source>, so the getter still returns the blob URL afterIMAsetvideo.src, the check misses, and cleanup removes theURLofAd. (On desktopChrome, where there is no<source>child, the detach check does fire, but the re-attach still overwrites thesrc.)Where the behaviour comes from (at
c3e5e8154)_onMediaSourceClose:src/controller/buffer-controller.ts#L1975-L2006readyState === 'ended' || 'closed':src/controller/buffer-controller.ts#L1057-L1068MEDIA_SOURCE_REQUIRES_RESETinonError:src/controller/error-controller.ts#L252-L258onErrorOut→recoverMediaError():src/controller/error-controller.ts#L477-L50651725fcc8), refined by IntroduceMEDIA_SOURCE_REQUIRES_RESETerror event (MediaSource closed while attached or ended on append error) #7699 (e1a424fe4), Track sourceended event and handle ManagedMediaSource recovery #7697 (23ed0dec7), HandleMEDIA_SOURCE_REQUIRES_RESETwith adaptation and reset #7702 (128d6fd33), MEDIA_SOURCE_REQUIRES_RESET improvements #7707 (d1a55884b); all first inv1.7.0-alpha.1, none inv1.6.19.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
mediaSrchonourHTMLresource selection (asrcattribute wins over<source>children):master...nochev:hls.js:fix/skip-media-source-reset-on-third-party-src
_onMediaSourceClose: ifmediaSrcChangedByThirdParty, warn and return (noMEDIA_SOURCE_REQUIRES_RESET).append-errorbranch: same guard, so the subsequent failing appends degrade exactly like1.6.x(bufferAppendError→ fatal →stopLoad) instead of resetting.mediaSrcgetter: prefermedia.srcwhen set, otherwise the<source>child.tests/unit/controller/buffer-controller.ts.Verified with a build of that branch on the same
iPhone(ManagedMediaSource), same steps:No
recoverMediaError(), no re-attach, noAbortError; the element is left to the third party exactly as in1.6.x. Also verified in desktopChrome(plainMediaSource), and in our production player (Video.js 8+videojs-ima,IMAcustom playback oniPhone) withVASTpre-, mid- and post-rollAds: behaviour matches1.6.19.PR: #8074 (happy to adjust it, e.g. stop loading immediately instead of letting the appends fail, if you prefer).
Console output
Chrome media internals output