Skip to content

fix: handle page restoration from bfcache in Safari#7683

Merged
robwalch merged 3 commits intovideo-dev:masterfrom
christriants:fix/reattach-media-pageshow
Jan 16, 2026
Merged

fix: handle page restoration from bfcache in Safari#7683
robwalch merged 3 commits intovideo-dev:masterfrom
christriants:fix/reattach-media-pageshow

Conversation

@christriants
Copy link
Copy Markdown
Collaborator

@christriants christriants commented Jan 5, 2026

This PR will...

This PR fixes an issue where video playback fails to resume after navigating back and forth between pages in Safari. The problem manifests as buffer append errors when reinitializing the player after a page navigation.

Why is this Pull Request needed?

The issue affects users who navigate away from a page with a playing HLS.js video and then return to it on Safari.

Are there any points in the code the reviewer needs to double check?

  • The solution should be tested across different browsers, with particular attention to Safari's behavior. Should we set these event listeners only for Safari?

Resolves issues:

Fixes #7578 - Unable to resume video playback after navigating back and forth between two pages

Demo of issue:
https://github.com/user-attachments/assets/d87ab24b-9774-474a-bbc1-a096d4d70009

Demo of fix:

Screen.Recording.2026-01-05.at.6.35.50.PM.mov

Checklist

  • Changes have been done against master branch, and PR does not conflict
  • New unit / functional tests have been added (whenever applicable)
  • API or design changes are documented in API.md

@robwalch
Copy link
Copy Markdown
Collaborator

robwalch commented Jan 5, 2026

I don't think we should apply this to all instances and browsers. The buffer controller would be the appropriate place to handle premature closing of the MediaSource, with recoverMediaError handling the detach/attach and loading. The source should not be reset in browsers that do not display this issue anywhere the MediaSource remains open.

It's also concerning that we're adding a global listener and never removing it. All listeners should be removed when instances are destroyed.

@christriants christriants marked this pull request as draft January 6, 2026 00:07
@christriants
Copy link
Copy Markdown
Collaborator Author

Thanks, @robwalch. Appreciate the feedback on the initial approach. I didn't realize recoverMediaError() already handled the detach/attach/loading flow, so I was implementing that manually.

I've removed the global listener, for pageshow, moved all logic for this change to the buffer-controller, and limited the scope of the change to Safari. My approach now is to invoke recoverMediaError on the MediaSource instance's sourceclose event only if we need to recover the premature closing that seems to occur with Safari's back/forward navigation.

@christriants christriants marked this pull request as ready for review January 6, 2026 04:50
Comment thread src/controller/buffer-controller.ts Outdated
Comment on lines +348 to +349
const handler = this._handleSafariMediaSourceClose.bind(this);
this.safariSourceCloseHandler = handler;
Copy link
Copy Markdown
Collaborator

@robwalch robwalch Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This binding is unnecessary with arrow function properties. private _handleSafariMediaSourceClose = () => will always be involved with the current class instance context for "this". (See other examples in the project like private onMediaSeeked = () =>.)

Comment thread src/controller/buffer-controller.ts Outdated
data: MediaAttachingData,
) {
const media = (this.media = data.media);
this.needsMediaSourceCloseRecovery = this.detectMediaSourceCloseIssue();
Copy link
Copy Markdown
Collaborator

@robwalch robwalch Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I didn't mean to suggest that this should be behind a user-agent check. I'd prefer to use existing listeners, or only add new event listeners (like "pageshow") as needed to detect that the source closure resulted from backgrounding the page.

We already have a listener for 'sourceclose': _onMediaSourceClose >

  • Can we simply use that to reopen when media is still attached and the player is not being destroyed?
  • Are there any other conditions that should be considered, like video.error, hls error, or loading state?
  • When is the MediaSource closed in relation to the page being backgrounded when the browser bug presents?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry for the misunderstanding! I think we have two solid options here.

  1. We can use the existing sourceclose listener / _onMediaSourceClose handler. On Safari, the MediaSource is closed when returning to the page via bfcache, so the sourceclose event fires immediately when we land back on the page. When this occurs, there's also a media error ("Load was aborted"), but I don't think that error is specific to this issue.

If media is still attached when sourceclose fires, we can trigger recovery. My only concern is understanding all scenarios where sourceclose may fire. Currently, the event also fires during media detachment, but we don't catch it because we remove the listener before the event is captured.

Something like this works:

private _onMediaSourceClose = () => {
  if (this.media) {
    this.hls.recoverMediaError();
  }
};

My concern is: what happens if we start catching the sourceclose event during media detachment in the future? I think we would try to trigger a recovery? But then again, this.media would be null, so the condition here probably protects us from this.

  1. We can add the pageshow listener from earlier to specifically account for bfcache restoration:
private _onPageShow = (event: PageTransitionEvent) => {
  if (event.persisted && this.mediaSource?.readyState === 'closed' && this.media) {
    this.hls.recoverMediaError();
  }
};

This is more explicit about handling bfcache restoration. On other browsers, the MediaSource readyState remains open when restoring from bfcache, so this check would prevent unnecessary recovery attempts. However, we would be adding a global window listener to all browsers and instances.

I pushed up a change for option 1, but happy to continue discussing.

@christriants christriants force-pushed the fix/reattach-media-pageshow branch from 7931295 to a7e5d32 Compare January 8, 2026 16:20
@robwalch
Copy link
Copy Markdown
Collaborator

Hi @christriants. Any chance you could verify whether or not these changes also address #7687? It sounds like the same root cause but with different repro steps on iOS. I confirmed that issue but haven't staged a build for testing.

@zalishchuk
Copy link
Copy Markdown
Contributor

This also significantly affects iOS in-app browsers such as Facebook or Instagram. Is there any workaround I can apply now to prevent playback issues and errors?

@robwalch
Copy link
Copy Markdown
Collaborator

This also significantly affects iOS in-app browsers such as Facebook or Instagram. Is there any workaround I can apply now to prevent playback issues and errors?

Hi @zalishchuk. You could always workaround this issue by calling this.hls.recoverMediaError(); this.hls.startLoad(); on BUFFER_APPEND_ERROR.

startLoad() is required only after a fatal error that results in stopLoad() being called. Calling it while loading is already active should be innocuous.

@christriants
Copy link
Copy Markdown
Collaborator Author

Hi, @robwalch. I just tested out the repro steps provided in #7687, and was still able to replicate the issue with this change applied. It also seems like it was introduced with iOS 26.2 (I wasn't able to replicate #7687 on 26.1).

@zalishchuk
Copy link
Copy Markdown
Contributor

zalishchuk commented Jan 15, 2026

@christriants The issue consistently occurs in Safari on iOS 26.2 when you clear the cache and load the page.

Sometimes, playback gets stuck because it waits for 3 failed buffer appends by default. However, it occasionally triggers 3 failed appends and then shows a fatal error. Behaves very strangely, to be honest.

stuck log:

[Log] [log] > – "attachMedia" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "created media source: ManagedMediaSource" (target.js, line 2)
[Log] [log] > – "stopLoad" (target.js, line 2)
[Log] [log] > – "loadSource:https://localhost:3001/stream.m3u8" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Trigger BUFFER_RESET" (target.js, line 2)
[Log] [log] > – "resume buffering" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "Media source opened" (target.js, line 2)
[Log] [log] > – "[level-controller]:" – "manifest loaded, 4 level(s) found, first bitrate: 4502691" (target.js, line 2)
[Log] [log] > – "setting initial bwe to 4502691" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "2 bufferCodec event(s) expected" (target.js, line 2)
[Log] [log] > – "set startLevel:2" (target.js, line 2)
[Log] [log] > – "startLoad(-1)" (target.js, line 2)
[Log] [log] > – "resume buffering" (target.js, line 2)
[Log] [log] > – "[level-controller]:" – "Switching to level 2 (720p SDR avc1,mp4a @2771086) from level -1" (target.js, line 2)
[Log] [log] > – "[audio-track-controller]:" – "Updating audio tracks, 1 track(s) found in group(s): audioTS" (target.js, line 2)
[Log] [log] > – "[audio-track-controller]:" – "Switching to audio-track 0 \"stream_2\" lang:undefined group:audioTS channels:2" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Reset loading state" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "STOPPED->IDLE" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "IDLE->WAITING_TRACK" (target.js, line 2)
[Log] [log] > – "[level-controller]:" – "Loading level index 2 with https://localhost:3001/variant_1280x72…" (target.js, line 2)
"Loading level index 2 with https://localhost:3001/variant_1280x720_2500000_variant.m3u8"
[Log] [log] > – "[stream-controller]:" – "STOPPED->IDLE" (target.js, line 2)
[Log] [log] > – "[audio-track-controller]:" – "loading audio-track playlist 0 \"stream_2\" lang:undefined group:audioTS" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "WAITING_TRACK->STOPPED" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "STOPPED->WAITING_TRACK" (target.js, line 2)
[Log] [log] > – "[subtitle-stream-controller]:" – "STOPPED->IDLE" (target.js, line 2)
[Log] [log] > – "resume buffering" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Level 2 loaded [0,53][part-53--1], cc [0, 0] duration:161.2" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "Updating Media Source duration to 161.200" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loading fragment 0 cc: 0 of [0-53] level: 2, target: 0" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[audio-track-controller]:" – "Audio track 0 \"stream_2\" lang:undefined group:audioTS loaded [0-53]" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Audio track 0 loaded [0,53][part-53--1],duration:161.32100000000014" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "WAITING_TRACK->IDLE" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loading fragment 0 cc: 0 of [0-53] track: 0, target: 0" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[transmuxer-interface, main]: Starting new transmux session for sn: 0 p: -1 level: 2 id: 1↵        discontinuity: true↵        trackSwitch…" (target.js, line 2)
"[transmuxer-interface, main]: Starting new transmux session for sn: 0 p: -1 level: 2 id: 1
        discontinuity: true
        trackSwitch: true
        contiguous: false
        accurateTimeOffset: true
        timeOffset: 0
        initSegmentChange: true"
[Log] [log] > – "[mp4-remuxer]: ISGenerated flag reset" (target.js, line 2)
[Log] [log] > – "[mp4-remuxer]: initPTS & initDTS reset" (target.js, line 2)
[Log] [log] > – "[mp4-remuxer]: reset next timestamp" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Init video buffer, container:video/mp4, codecs[level/parsed]=[avc1.4d401f/avc1.4d401f]" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "1 bufferCodec event(s) expected video" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "InitPTS for cc: 0 found from main: 72000" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loaded fragment 0 of level 2" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 0 of level 2" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[transmuxer-interface, audio]: Starting new transmux session for sn: 0 p: -1 level: 0 id: 1↵        discontinuity: true↵      …" (target.js, line 2)
"[transmuxer-interface, audio]: Starting new transmux session for sn: 0 p: -1 level: 0 id: 1
        discontinuity: true
        trackSwitch: true
        contiguous: false
        accurateTimeOffset: false
        timeOffset: 0
        initSegmentChange: true"
[Log] [log] > – "[mp4-remuxer]: ISGenerated flag reset" (target.js, line 2)
[Log] [log] > – "[mp4-remuxer]: initPTS & initDTS reset" (target.js, line 2)
[Log] [log] > – "[mp4-remuxer]: reset next timestamp" (target.js, line 2)
[Log] [log] > – "manifest codec:mp4a.40.2, ADTS type:2, samplingIndex:4" (target.js, line 2)
[Log] [log] > – "parsed codec:mp4a.40.5, rate:44100, channels:2" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Init audio buffer, container:audio/mp4, codecs[level/parsed]=[mp4a.40.2/mp4a.40.5]" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "0 bufferCodec event(s) expected audio" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "creating sourceBuffer(video/mp4;codecs=avc1.4d401f)" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "creating sourceBuffer(audio/mp4;codecs=mp4a.40.2)" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Alternate track found, use video.buffered to schedule main fragment loading" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loaded fragment 0 of level 0" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 0 of level 0" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Error] [error] > – "[buffer-controller]" – "Error: video SourceBuffer error. MediaSource readyState: ended" – Event {isTrusted: true, type: "error", target: ManagedSourceBuffer, …}
Event {isTrusted: true, type: "error", target: ManagedSourceBuffer, currentTarget: ManagedSourceBuffer, eventPhase: 2, …}Event
	(anonymous function) (target.js:2:333311)
	_onSBUpdateError (280bb671-3905-40ad-ab48-a7c16622ff9c:27330)
[Warning] [warn] > – "[buffer-controller]" – "Failed 1/3 times to append segment in \"video\" sourceBuffer" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Reset loading state" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Warning] [warn] > – "[warning]:" – "switching to level 1 after bufferAppendError" (target.js, line 2)
[Log] [log] > – "[level-controller]:" – "Switching to level 1 (360p SDR avc1,mp4a @727651) from level 2" (target.js, line 2)
[Log] [log] > – "[level-controller]:" – "Loading level index 1 with https://localhost:3001/variant_640x360…" (target.js, line 2)
"Loading level index 1 with https://localhost:3001/variant_640x360_1500000_variant.m3u8"
[Log] [log] > – "[stream-controller]:" – "IDLE->WAITING_LEVEL" (target.js, line 2)
[Warning] [warn] > – "[warning]:" – "MediaSource ended after \"video\" sourceBuffer append error. Attempting to recover from media error." (target.js, line 2)
[Log] [log] > – "recoverMediaError" (target.js, line 2)
[Log] [log] > – "detachMedia" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "media source detaching" (target.js, line 2)
[Log] [log] > – "resume buffering" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "WAITING_LEVEL->STOPPED" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSED->STOPPED" (target.js, line 2)
[Log] [log] > – "[subtitle-stream-controller]:" – "IDLE->STOPPED" (target.js, line 2)
[Log] [log] > – "attachMedia" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "created media source: ManagedMediaSource" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "Media source opened" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "Updating Media Source duration to 161.200" (target.js, line 2)
[Log] [log] > – "resume buffering" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Level 1 loaded [0,53][part-53--1], cc [0, 0] duration:161.2" (target.js, line 2)

fatal log:

[Log] [log] > – "Debug logs enabled for \"Hls instance\" in hls.js version 1.5.20" (target.js, line 2)
[Log] [log] > – "attachMedia" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "created media source: ManagedMediaSource" (target.js, line 2)
[Log] [log] > – "stopLoad" (target.js, line 2)
[Log] [log] > – "loadSource:https://localhost:3001/stream.m3u8" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Trigger BUFFER_RESET" (target.js, line 2)
[Log] [log] > – "resume buffering" (target.js, line 2)
[Log] [log] > – "[level-controller]:" – "manifest loaded, 4 level(s) found, first bitrate: 4502691" (target.js, line 2)
[Log] [log] > – "setting initial bwe to 4502691" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "2 bufferCodec event(s) expected" (target.js, line 2)
[Log] [log] > – "set startLevel:2" (target.js, line 2)
[Log] [log] > – "startLoad(-1)" (target.js, line 2)
[Log] [log] > – "resume buffering" (target.js, line 2)
[Log] [log] > – "[level-controller]:" – "Switching to level 2 (720p SDR avc1,mp4a @2771086) from level -1" (target.js, line 2)
[Log] [log] > – "[audio-track-controller]:" – "Updating audio tracks, 1 track(s) found in group(s): audioTS" (target.js, line 2)
[Log] [log] > – "[audio-track-controller]:" – "Switching to audio-track 0 \"stream_2\" lang:undefined group:audioTS channels:2" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Reset loading state" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "STOPPED->IDLE" (target.js, line 2)
[Log] [log] > – "[level-controller]:" – "Loading level index 2 with https://localhost:3001/variant_1280x72…" (target.js, line 2)
"Loading level index 2 with https://localhost:3001/variant_1280x720_2500000_variant.m3u8"
[Log] [log] > – "[stream-controller]:" – "STOPPED->IDLE" (target.js, line 2)
[Log] [log] > – "[audio-track-controller]:" – "loading audio-track playlist 0 \"stream_2\" lang:undefined group:audioTS" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "IDLE->STOPPED" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "STOPPED->WAITING_TRACK" (target.js, line 2)
[Log] [log] > – "[subtitle-stream-controller]:" – "STOPPED->IDLE" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "Media source opened" (target.js, line 2)
[Log] [log] > – "resume buffering" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Level 2 loaded [0,53][part-53--1], cc [0, 0] duration:161.2" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "Updating Media Source duration to 161.200" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loading fragment 0 cc: 0 of [0-53] level: 2, target: 0" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[audio-track-controller]:" – "Audio track 0 \"stream_2\" lang:undefined group:audioTS loaded [0-53]" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Audio track 0 loaded [0,53][part-53--1],duration:161.32100000000014" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "WAITING_TRACK->IDLE" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loading fragment 0 cc: 0 of [0-53] track: 0, target: 0" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[transmuxer-interface, main]: Starting new transmux session for sn: 0 p: -1 level: 2 id: 1↵        discontinuity: true↵        trackSwitch…" (target.js, line 2)
"[transmuxer-interface, main]: Starting new transmux session for sn: 0 p: -1 level: 2 id: 1
        discontinuity: true
        trackSwitch: true
        contiguous: false
        accurateTimeOffset: true
        timeOffset: 0
        initSegmentChange: true"
[Log] [log] > – "[mp4-remuxer]: ISGenerated flag reset" (target.js, line 2)
[Log] [log] > – "[mp4-remuxer]: initPTS & initDTS reset" (target.js, line 2)
[Log] [log] > – "[mp4-remuxer]: reset next timestamp" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Init video buffer, container:video/mp4, codecs[level/parsed]=[avc1.4d401f/avc1.4d401f]" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "1 bufferCodec event(s) expected video" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "InitPTS for cc: 0 found from main: 72000" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loaded fragment 0 of level 2" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 0 of level 2" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[transmuxer-interface, audio]: Starting new transmux session for sn: 0 p: -1 level: 0 id: 1↵        discontinuity: true↵      …" (target.js, line 2)
"[transmuxer-interface, audio]: Starting new transmux session for sn: 0 p: -1 level: 0 id: 1
        discontinuity: true
        trackSwitch: true
        contiguous: false
        accurateTimeOffset: false
        timeOffset: 0
        initSegmentChange: true"
[Log] [log] > – "[mp4-remuxer]: ISGenerated flag reset" (target.js, line 2)
[Log] [log] > – "[mp4-remuxer]: initPTS & initDTS reset" (target.js, line 2)
[Log] [log] > – "[mp4-remuxer]: reset next timestamp" (target.js, line 2)
[Log] [log] > – "manifest codec:mp4a.40.2, ADTS type:2, samplingIndex:4" (target.js, line 2)
[Log] [log] > – "parsed codec:mp4a.40.5, rate:44100, channels:2" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Init audio buffer, container:audio/mp4, codecs[level/parsed]=[mp4a.40.2/mp4a.40.5]" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "0 bufferCodec event(s) expected audio" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "creating sourceBuffer(video/mp4;codecs=avc1.4d401f)" (target.js, line 2)
[Log] [log] > – "[buffer-controller]" – "creating sourceBuffer(audio/mp4;codecs=mp4a.40.2)" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Alternate track found, use video.buffered to schedule main fragment loading" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loaded fragment 0 of level 0" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 0 of level 0" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Buffered audio sn: 0 of track 0 (frag:[0.000-3.019] > buffer:[0.000-3.019])" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loading fragment 1 cc: 0 of [0-53] track: 0, target: 3.042" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loaded fragment 1 of level 0" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 1 of level 0" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Buffered main sn: 0 of level 2 (frag:[0.000-3.000] > buffer:[0.000-3.000])" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Info] [info] > – "[abr] switch candidate:2->3 adjustedbw(57798720)-bitrate=53296029 ttfb:0.0 avgDuration:3.0 maxFetchDuration:3.0 fetchDuration:0.…" (target.js, line 2)
"[abr] switch candidate:2->3 adjustedbw(57798720)-bitrate=53296029 ttfb:0.0 avgDuration:3.0 maxFetchDuration:3.0 fetchDuration:0.3 firstSelection:false codecSet:avc1,mp4a videoRange:SDR hls.loadLevel:2"
[Log] [log] > – "[stream-controller]:" – "Adapting to level 3 from level 2" (target.js, line 2)
[Log] [log] > – "[level-controller]:" – "Switching to level 3 (1080p SDR avc1,mp4a @4502691) from level 2" (target.js, line 2)
[Log] [log] > – "[level-controller]:" – "Loading level index 3 with https://localhost:3001/variant_1920x10…" (target.js, line 2)
"Loading level index 3 with https://localhost:3001/variant_1920x1080_4000000_variant.m3u8"
[Log] [log] > – "[stream-controller]:" – "IDLE->WAITING_LEVEL" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Level 3 loaded [0,53][part-53--1], cc [0, 0] duration:161.2" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "WAITING_LEVEL->IDLE" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loading fragment 1 cc: 0 of [0-53] level: 3, target: 3" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[transmuxer-interface, main]: Starting new transmux session for sn: 1 p: -1 level: 3 id: 1↵        discontinuity: false↵      …" (target.js, line 2)
"[transmuxer-interface, main]: Starting new transmux session for sn: 1 p: -1 level: 3 id: 1
        discontinuity: false
        trackSwitch: true
        contiguous: false
        accurateTimeOffset: true
        timeOffset: 3
        initSegmentChange: false"
[Log] [log] > – "[mp4-remuxer]: ISGenerated flag reset" (target.js, line 2)
[Log] [log] > – "[mp4-remuxer]: initPTS & initDTS reset" (target.js, line 2)
[Log] [log] > – "[mp4-remuxer]: reset next timestamp" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Init video buffer, container:video/mp4, codecs[level/parsed]=[avc1.4d4028/avc1.4d4028]" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loaded fragment 1 of level 3" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 1 of level 3" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Buffered audio sn: 1 of track 0 (frag:[3.019-6.014] > buffer:[0.000-6.014])" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loading fragment 2 cc: 0 of [0-53] track: 0, target: 6.014" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loaded fragment 2 of level 0" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 2 of level 0" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Buffered audio sn: 2 of track 0 (frag:[6.014-9.009] > buffer:[0.000-9.009])" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Buffered main sn: 1 of level 3 (frag:[3.000-6.000] > buffer:[0.000-6.000])" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loading fragment 2 cc: 0 of [0-53] level: 3, target: 6" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loaded fragment 2 of level 3" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 2 of level 3" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Buffered main sn: 2 of level 3 (frag:[6.000-9.000] > buffer:[0.000-9.000])" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loading fragment 3 cc: 0 of [0-53] level: 3, target: 9" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loaded fragment 3 of level 3" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 3 of level 3" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Buffered main sn: 3 of level 3 (frag:[9.000-12.000] > buffer:[0.000-12.000])" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loading fragment 4 cc: 0 of [0-53] level: 3, target: 12" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loaded fragment 4 of level 3" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 4 of level 3" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Buffered main sn: 4 of level 3 (frag:[12.000-15.000] > buffer:[0.000-15.000])" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loading fragment 5 cc: 0 of [0-53] level: 3, target: 15" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loaded fragment 5 of level 3" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 5 of level 3" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Buffered main sn: 5 of level 3 (frag:[15.000-18.000] > buffer:[0.000-18.000])" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loading fragment 6 cc: 0 of [0-53] level: 3, target: 18" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loaded fragment 6 of level 3" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 6 of level 3" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Buffered main sn: 6 of level 3 (frag:[18.000-21.000] > buffer:[0.000-21.000])" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loading fragment 7 cc: 0 of [0-53] level: 3, target: 21" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loaded fragment 7 of level 3" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 7 of level 3" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Buffered main sn: 7 of level 3 (frag:[21.000-24.000] > buffer:[0.000-24.000])" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loading fragment 8 cc: 0 of [0-53] level: 3, target: 24" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loaded fragment 8 of level 3" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 8 of level 3" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Buffered main sn: 8 of level 3 (frag:[24.000-27.000] > buffer:[0.000-27.000])" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loading fragment 9 cc: 0 of [0-53] level: 3, target: 27" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loaded fragment 9 of level 3" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 9 of level 3" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loading fragment 3 cc: 0 of [0-53] track: 0, target: 9.009" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loaded fragment 3 of level 0" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 3 of level 0" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Buffered audio sn: 3 of track 0 (frag:[9.009-12.005] > buffer:[0.000-12.005])" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loading fragment 4 cc: 0 of [0-53] track: 0, target: 12.005" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loaded fragment 4 of level 0" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 4 of level 0" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Buffered main sn: 9 of level 3 (frag:[27.000-30.000] > buffer:[0.000-30.000])" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loading fragment 10 cc: 0 of [0-53] level: 3, target: 30" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Loaded fragment 10 of level 3" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 10 of level 3" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Buffered audio sn: 4 of track 0 (frag:[12.005-15.000] > buffer:[0.000-15.000])" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loading fragment 5 cc: 0 of [0-53] track: 0, target: 15" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loaded fragment 5 of level 0" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 5 of level 0" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "Buffered main sn: 10 of level 3 (frag:[30.000-33.000] > buffer:[0.000-33.000])" (target.js, line 2)
[Log] [log] > – "[stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Buffered audio sn: 5 of track 0 (frag:[15.000-18.019] > buffer:[0.000-18.019])" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loading fragment 6 cc: 0 of [0-53] track: 0, target: 18.019" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loaded fragment 6 of level 0" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 6 of level 0" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Buffered audio sn: 6 of track 0 (frag:[18.019-21.014] > buffer:[0.000-21.014])" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loading fragment 7 cc: 0 of [0-53] track: 0, target: 21.014" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loaded fragment 7 of level 0" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 7 of level 0" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Buffered audio sn: 7 of track 0 (frag:[21.014-24.009] > buffer:[0.000-24.009])" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loading fragment 8 cc: 0 of [0-53] track: 0, target: 24.009" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loaded fragment 8 of level 0" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 8 of level 0" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Buffered audio sn: 8 of track 0 (frag:[24.009-27.005] > buffer:[0.000-27.005])" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSED->IDLE" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loading fragment 9 cc: 0 of [0-53] track: 0, target: 27.005" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "IDLE->FRAG_LOADING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "FRAG_LOADING->PARSING" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Loaded fragment 9 of level 0" (target.js, line 2)
[Log] [log] > – "[transmuxer.ts]: Flushed fragment 9 of level 0" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSING->PARSED" (target.js, line 2)
[Log] [log] > – "pause buffering" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "Buffered audio sn: 9 of track 0 (frag:[27.005-30.000] > buffer:[0.000-30.000])" (target.js, line 2)
[Log] [log] > – "[audio-stream-controller]:" – "PARSED->IDLE" (target.js, line 2)

@robwalch
Copy link
Copy Markdown
Collaborator

robwalch commented Jan 15, 2026

However, it occasionally triggers 3 failed appends and then shows a fatal error.

appendErrorMaxRetry defaults to 3:
https://github.com/video-dev/hls.js/blob/v1.6.15/docs/API.md#appenderrormaxretry

stuck log:

You need to call hls.startLoad() when hls.loadingEnabled is false (after fatal error and logs show stream controller state changed to ->STOPPED).

fatal log:

What is fatal about it? There are no errors in the logs. [log] > – "pause buffering" indicates that streaming of segments was paused - probably by ManagedMediaSource "endstreaming". If hls.bufferingEnabled is false, call hls.resumeBuffering() to resume segment loading.

This and the previous comments related to #7687 should be included in that issue.

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Unable to resume video playback after navigating back and forth between two pages

3 participants