Skip to content

Conversation

@zalishchuk
Copy link

This PR will...

Fix ManagedMediaSource cold start bug on iOS 26.2 by detecting the broken state and triggering immediate recovery.

Why is this Pull Request needed?

On iOS 26.2 real devices, the first ManagedMediaSource instance after a browser cold start fails when concurrent appendBuffer() calls are made. iOS 26.0 and 26.1 are not affected.

Bug behavior:

  • readyState reports ended even though it's not truly ended
  • The sourceended event never fires (unlike legitimate ends)
  • One concurrent append succeeds, the other fails
  • The second MMS instance always works

WebKit Bug: https://bugs.webkit.org/show_bug.cgi?id=305712

Stuck vs Non-Stuck Randomness:

The bug causes one of two SourceBuffer appends to fail randomly. Whether playback gets stuck depends on which append succeeds:

Outcome Which SB errored Which succeeded first Buffer start Result
STUCK audio video 4.223s (keyframe) Stuck at position 0
NOT STUCK video audio 0.023s Playback works
  • Video data typically starts at a keyframe which may not be at exactly 0s
  • Audio can start from the beginning of the segment
  • When video succeeds but audio fails, buffer starts at first video keyframe (~4.2s)
  • Player is stuck because currentTime=0 but buffer starts at 4.2s
  • When the user taps play again, hls.js seeks over the gap and video starts from ~4.2s instead of 0s
Log Excerpts

STUCK Case (audio error, video succeeded first):

// Bug triggers - audio SourceBuffer fails
[Error] "Error: audio SourceBuffer error. MediaSource readyState: ended"

// Error controller does level switching before recovery (unnecessary)
[Warning] "[error-controller]:" - "switching to level 1 after bufferAppendError"
[Warning] "[error-controller]:" - "MediaSource ended after \"audio\" sourceBuffer append error. Attempting to recover from media error."

// Recovery happens
[Log] "recoverMediaError"
[Log] "[buffer-controller]:" - "created media source: ManagedMediaSource"

// After recovery - buffer has gap at start (PROBLEM)
[Log] "Buffered main sn: 0 of level 1 (frag:[0.000-10.031] > buffer:[4.223-9.990])"
//                                                                  ^^^^^ starts at 4.2s, not 0!

NOT STUCK Case (video error, audio succeeded first):

// Bug triggers - video SourceBuffer fails
[Error] "Error: video SourceBuffer error. MediaSource readyState: ended"

// Same recovery flow...
[Log] "recoverMediaError"
[Log] "[buffer-controller]:" - "created media source: ManagedMediaSource"

// After recovery - buffer starts near 0 (OK)
[Log] "Buffered main sn: 0 of level 2 (frag:[0.000-10.023] > buffer:[0.023-10.008])"
//                                                                  ^^^^^ starts at 0.02s

With Fix (expected behavior):

// Bug detected immediately in buffer-controller
[Warning] "[buffer-controller]:" - "ManagedMediaSource readyState \"ended\" without sourceended event - triggering recovery"

// Immediate recovery, no level switching
[Log] "recoverMediaError"
[Log] "[buffer-controller]:" - "created media source: ManagedMediaSource"

// Playback continues normally

Fix approach:

  • Track whether sourceended event has fired
  • If readyState === "ended" but sourceended never fired -> bug detected
  • Trigger immediate recoverMediaError() at the source, before the error propagates through error-controller
  • Pure state/feature detection (no user agent sniffing)
  • Matches existing hls.js pattern (similar to bfcache fix in _onMediaSourceClose)

Note

Without this fix, error-controller attempts level switching before eventually calling recoverMediaError(). Since the MediaSource itself is broken, early detection allows faster recovery.

[Warning] "[error-controller]:" - "switching to level 1 after bufferAppendError"
[Warning] "[error-controller]:" - "MediaSource ended after \"audio\" sourceBuffer append error. Attempting to recover from media error."

Screencasts

Before (bug):

no_fix.mp4

After (fix):

with_fix.mp4

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

The detection logic in onSBUpdateError:

if (this.appendSource && readyState === 'ended' && !this.sourceEnded) {
  // ...
}

This should only trigger when:

  1. Using ManagedMediaSource (this.appendSource)
  2. readyState is ended
  3. sourceended event never fired (!this.sourceEnded)

Resolves issues:

Fixes #7687

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
robwalch previously approved these changes Jan 21, 2026
@zalishchuk
Copy link
Author

A quick breakdown on why this detection is safe.

We check if readyState is ended but sourceended event never fired. Here's why this won't cause false positives.

How it normally works (per W3C spec)

When readyState becomes ended:

  1. It changes synchronously
  2. sourceended event gets queued
  3. It stays ended until MediaSource is detached

Append errors also trigger "end of stream" per spec, so sourceended should fire.

What iOS 26.2 does (broken)

  1. During error callback -> readyState shows ended
  2. After callback -> readyState goes back to open
  3. sourceended never fires

This flickering state is not spec-compliant.

Case readyState sourceEnded Triggers?
Normal playback open false No
Legit end of stream ended true No
Normal append error ended true No
iOS 26.2 bug ended false Yes

@zalishchuk
Copy link
Author

Went with sourceReadyState: ReadyState | null instead of a simple sourceEnded: boolean.

The detection logic is the same - we check if readyState === 'ended' but sourceReadyState !== 'ended' (meaning sourceended never fired).

But tracking the full state felt cleaner. It's more self-documenting when reading the check, syncs with the actual ms.readyState at creation, and we already listen to all three events anyway (sourceopen, sourceended, sourceclose).

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

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

bufferAppendError on iOS 26.2 when autoStartLoad=false

2 participants