Skip to content

Do not recover a media error that is already fatal - #3

Closed
hongjun-bae wants to merge 1 commit into
masterfrom
fix/no-media-recovery-on-fatal-append-error
Closed

hongjun-bae wants to merge 1 commit into
masterfrom
fix/no-media-recovery-on-fatal-append-error

Conversation

@hongjun-bae

Copy link
Copy Markdown
Owner

This PR will...

Stop onErrorOut from calling recoverMediaError() for an error it is about to treat as fatal.

Why is this Pull Request needed?

onErrorOut runs both branches in the same pass:

const flags = data.errorAction?.flags || 0;
if (flags & ErrorActionFlags.ResetMediaSource) {
  this.hls.recoverMediaError();
}

if (data.fatal) {
  this.hls.stopLoad();
  return;
}

recoverMediaError() detaches and re-attaches the media and calls startLoad(time) when loading had started. The re-attach is asynchronous, so BaseStreamController.onMediaAttached runs after the synchronous stopLoad() and starts loading again while autoStartLoad is set, which is the default. The player keeps requesting the segment the SourceBuffer rejected after it has told the application the error is fatal.

On a single variant stream this becomes a loop. MEDIA_SOURCE_REQUIRES_RESET has no alternate level to switch to, so errorAction.resolved stays false, every rejection is promoted to fatal in the branch above, and the flag check restarts it. Measured on a fork of the v1.6 line with video-dev#7699, video-dev#7702 and video-dev#7707 backported, one deliberately corrupted fMP4 segment on a live single variant stream: nine self recoveries, nine fatal errors delivered to the application, nine refusals by the application's own recovery, one of each per cycle.

Before video-dev#7702 the two were mutually exclusive:

if (!data.errorAction.resolved && data.details !== ErrorDetails.FRAG_GAP) {
  data.fatal = true;
} else if (/MediaSource readyState: ended/.test(data.error.message)) {
  this.hls.recoverMediaError();
}

This restores that: recovery of a fatal error belongs to the application. Non fatal paths are unchanged, so a MEDIA_SOURCE_REQUIRES_RESET that a level switch resolves still resets the MediaSource.

Related to video-dev#8035. It is independent of video-dev#8045, which decides whether the fragment is skipped; this decides only what happens once an error is already fatal.

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

The existing unit test treats MEDIA_SOURCE_REQUIRES_RESET as recoverable and calls recoverMediaError covered the no-alternate case, where onErrorOut promotes the error to fatal before the flag is read. It is replaced by two tests, one per side of the condition:

  • a resolved action still calls recoverMediaError()
  • a promoted-to-fatal error does not, and stopLoad() is called

If the intent was that a fatal MEDIA_SOURCE_REQUIRES_RESET should still rebuild the MediaSource, the fix belongs on the other side instead: stopLoad() would have to survive the re-attach. Happy to reshape it that way.

Resolves issues:

Related to video-dev#8035

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

`onErrorOut` calls `recoverMediaError()` whenever the error action carries
`ErrorActionFlags.ResetMediaSource`, then calls `stopLoad()` on the next line when
the error is fatal. The two undo each other.

`recoverMediaError()` detaches and re-attaches the media, and calls `startLoad(time)`
when loading had started (hls.ts). The re-attach is asynchronous, so the
`MEDIA_ATTACHED` handler in `base-stream-controller` runs after the synchronous
`stopLoad()` and starts loading again while `autoStartLoad` is set, which is the
default. The player keeps requesting the segment the SourceBuffer rejected after it
has told the application the error is fatal.

On a single variant stream this is a loop. `MEDIA_SOURCE_REQUIRES_RESET` has no
alternate level to switch to, so `errorAction.resolved` stays false and every
rejection is promoted to fatal in the branch above. Measured on a fork of the v1.6
line with the reset backported: nine self recoveries, nine fatal errors delivered to
the application and nine refusals by its own recovery, one per cycle.

Before video-dev#7702 the two were mutually exclusive:

    if (!resolved && details !== FRAG_GAP) data.fatal = true;
    else if (/MediaSource readyState: ended/.test(data.error.message)) recoverMediaError();

Restore that by skipping the reset once the error is fatal. Recovery of a fatal error
belongs to the application. Non fatal paths are unchanged.

The existing unit test covered the no alternate case, where the error is promoted to
fatal before the flag is read. It is replaced by two tests, one for each side of the
condition.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant