diff --git a/src/controller/buffer-controller.ts b/src/controller/buffer-controller.ts index 5dd02c127e8..95bb6617b7b 100755 --- a/src/controller/buffer-controller.ts +++ b/src/controller/buffer-controller.ts @@ -1575,6 +1575,15 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe private _onMediaSourceClose = () => { this.log('Media source closed'); + // Safari/WebKit bug: MediaSource becomes invalid after bfcache restoration. + // When the user navigates back, the MediaSource is in a 'closed' state and cannot be used. + // If sourceclose fires while media is still attached, trigger recovery to reattach media. + if (this.media) { + this.warn( + 'MediaSource closed while media attached - triggering recovery', + ); + this.hls.recoverMediaError(); + } }; private _onMediaSourceEnded = () => { diff --git a/tests/unit/controller/buffer-controller.ts b/tests/unit/controller/buffer-controller.ts index 86994802693..16e5f658bc9 100644 --- a/tests/unit/controller/buffer-controller.ts +++ b/tests/unit/controller/buffer-controller.ts @@ -57,6 +57,7 @@ type BufferControllerTestable = Omit< sourceBuffers: SourceBuffersTuple; tracks: SourceBufferTrackSet; tracksReady: boolean; + _onMediaSourceClose: () => void; }; describe('BufferController', function () { @@ -557,4 +558,25 @@ describe('BufferController', function () { expect(bufferController.bufferedToEnd).to.be.true; }); }); + + describe('Safari MediaSource bfcache close recovery', function () { + it('triggers recoverMediaError when sourceclose fires with media attached', function () { + const media = new MockMediaElement() as unknown as HTMLMediaElement; + const mediaSource = new MockMediaSource() as unknown as MediaSource; + const recoverMediaErrorSpy = sandbox.spy(hls, 'recoverMediaError'); + bufferController.media = media; + bufferController.mediaSource = mediaSource; + bufferController._onMediaSourceClose(); + expect(recoverMediaErrorSpy).to.have.been.calledOnce; + }); + + it('does not trigger recovery when sourceclose fires without media attached', function () { + const mediaSource = new MockMediaSource() as unknown as MediaSource; + const recoverMediaErrorSpy = sandbox.spy(hls, 'recoverMediaError'); + bufferController.media = null; + bufferController.mediaSource = mediaSource; + bufferController._onMediaSourceClose(); + expect(recoverMediaErrorSpy).to.not.have.been.called; + }); + }); });