From 981875c35a44f82559b144538c6d3e9997d3af5c Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Fri, 24 Feb 2023 17:08:01 -0800 Subject: [PATCH] Retry audio and subtitle playlist loading after failure on level switch when no alternate is available --- api-extractor/report/hls.js.api.md | 6 ++-- src/controller/audio-track-controller.ts | 4 +++ src/controller/base-playlist-controller.ts | 16 +++++++-- src/controller/error-controller.ts | 40 +++++++++++++-------- src/controller/subtitle-track-controller.ts | 11 +++--- 5 files changed, 53 insertions(+), 24 deletions(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index e7af4df8f01..c02b5fe0eb2 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -234,7 +234,9 @@ export class BasePlaylistController implements NetworkComponentAPI { // (undocumented) protected requestScheduled: number; // (undocumented) - protected shouldLoadPlaylist(playlist: Level | MediaPlaylist): boolean; + protected shouldLoadPlaylist(playlist: Level | MediaPlaylist | null | undefined): boolean; + // (undocumented) + protected shouldReloadPlaylist(playlist: Level | MediaPlaylist | null | undefined): boolean; // (undocumented) startLoad(): void; // (undocumented) @@ -903,8 +905,6 @@ export class ErrorController implements NetworkComponentAPI { // (undocumented) onErrorOut(event: Events.ERROR, data: ErrorData): void; // (undocumented) - sendAlternateToPenaltyBox(data: ErrorData): void; - // (undocumented) startLoad(startPosition: number): void; // (undocumented) stopLoad(): void; diff --git a/src/controller/audio-track-controller.ts b/src/controller/audio-track-controller.ts index dd149fb77b8..e08f4b99cfc 100644 --- a/src/controller/audio-track-controller.ts +++ b/src/controller/audio-track-controller.ts @@ -141,6 +141,9 @@ class AudioTrackController extends BasePlaylistController { this.hls.trigger(Events.AUDIO_TRACKS_UPDATED, audioTracksUpdated); this.selectInitialTrack(); + } else if (this.shouldReloadPlaylist(this.currentTrack)) { + // Retry playlist loading if no playlist is or has been loaded yet + this.setAudioTrack(this.trackId); } } @@ -154,6 +157,7 @@ class AudioTrackController extends BasePlaylistController { data.context.id === this.trackId && data.context.groupId === this.groupId ) { + this.requestScheduled = -1; this.checkRetry(data); } } diff --git a/src/controller/base-playlist-controller.ts b/src/controller/base-playlist-controller.ts index e69a2d1f211..db0ce83ef58 100644 --- a/src/controller/base-playlist-controller.ts +++ b/src/controller/base-playlist-controller.ts @@ -106,15 +106,27 @@ export default class BasePlaylistController implements NetworkComponentAPI { // Loading is handled by the subclasses } - protected shouldLoadPlaylist(playlist: Level | MediaPlaylist): boolean { + protected shouldLoadPlaylist( + playlist: Level | MediaPlaylist | null | undefined + ): boolean { return ( this.canLoad && - playlist && + !!playlist && !!playlist.url && (!playlist.details || playlist.details.live) ); } + protected shouldReloadPlaylist( + playlist: Level | MediaPlaylist | null | undefined + ): boolean { + return ( + this.timer === -1 && + this.requestScheduled === -1 && + this.shouldLoadPlaylist(playlist) + ); + } + protected playlistLoaded( index: number, data: LevelLoadedData | AudioTrackLoadedData | TrackLoadedData, diff --git a/src/controller/error-controller.ts b/src/controller/error-controller.ts index b5f9ae5b51b..0bdf1a0528d 100644 --- a/src/controller/error-controller.ts +++ b/src/controller/error-controller.ts @@ -118,7 +118,6 @@ export default class ErrorController implements NetworkComponentAPI { case ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT: case ErrorDetails.SUBTITLE_LOAD_ERROR: case ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT: - // Switch to redundant level when track fails to load if (context) { const level = hls.levels[hls.loadLevel]; if ( @@ -130,11 +129,16 @@ export default class ErrorController implements NetworkComponentAPI { level.textGroupIds && context.groupId === level.textGroupIds[level.urlId])) ) { - // redundant failover - data.errorAction = { - action: NetworkErrorAction.SendAlternateToPenaltyBox, - flags: ErrorActionFlags.MoveAllAlternatesMatchingHost, - }; + // Perform Pathway switch or Redundant failover if possible for fastest recovery + // otherwise allow playlist retry count to reach max error retries + data.errorAction = this.getPlaylistRetryOrSwitchAction( + data, + hls.loadLevel + ); + data.errorAction.action = + NetworkErrorAction.SendAlternateToPenaltyBox; + data.errorAction.flags = + ErrorActionFlags.MoveAllAlternatesMatchingHost; return; } } @@ -335,7 +339,7 @@ export default class ErrorController implements NetworkComponentAPI { } } - sendAlternateToPenaltyBox(data: ErrorData) { + private sendAlternateToPenaltyBox(data: ErrorData) { const hls = this.hls; const errorAction = data.errorAction; if (!errorAction) { @@ -345,13 +349,7 @@ export default class ErrorController implements NetworkComponentAPI { switch (flags) { case ErrorActionFlags.None: - if (nextAutoLevel !== undefined) { - this.warn(`${data.details}: switching to level ${nextAutoLevel}`); - this.hls.nextAutoLevel = nextAutoLevel; - errorAction.resolved = true; - // Stream controller is responsible for this but won't switch on false start - this.hls.nextLoadLevel = this.hls.nextAutoLevel; - } + this.switchLevel(data, nextAutoLevel); break; case ErrorActionFlags.MoveAllAlternatesMatchingHost: { @@ -375,6 +373,20 @@ export default class ErrorController implements NetworkComponentAPI { ); break; } + // If not resolved by previous actions try to switch to next level + if (!errorAction.resolved) { + this.switchLevel(data, nextAutoLevel); + } + } + + private switchLevel(data: ErrorData, levelIndex: number | undefined) { + if (levelIndex !== undefined && data.errorAction) { + this.warn(`${data.details}: switching to level ${levelIndex}`); + this.hls.nextAutoLevel = levelIndex; + data.errorAction.resolved = true; + // Stream controller is responsible for this but won't switch on false start + this.hls.nextLoadLevel = this.hls.nextAutoLevel; + } } private redundantFailover(levelIndex: number): boolean { diff --git a/src/controller/subtitle-track-controller.ts b/src/controller/subtitle-track-controller.ts index 8d07f8c8e71..2366af75704 100644 --- a/src/controller/subtitle-track-controller.ts +++ b/src/controller/subtitle-track-controller.ts @@ -198,13 +198,11 @@ class SubtitleTrackController extends BasePlaylistController { if (!levelInfo?.textGroupIds) { return; } - const textGroupId = levelInfo.textGroupIds[levelInfo.urlId]; + const lastTrack = this.tracksInGroup + ? this.tracksInGroup[this.trackId] + : undefined; if (this.groupId !== textGroupId) { - const lastTrack = this.tracksInGroup - ? this.tracksInGroup[this.trackId] - : undefined; - const subtitleTracks = this.tracks.filter( (track): boolean => !textGroupId || track.groupId === textGroupId ); @@ -224,6 +222,9 @@ class SubtitleTrackController extends BasePlaylistController { if (initialTrackId !== -1) { this.setSubtitleTrack(initialTrackId, lastTrack); } + } else if (this.shouldReloadPlaylist(lastTrack)) { + // Retry playlist loading if no playlist is or has been loaded yet + this.setSubtitleTrack(this.trackId, lastTrack); } }