diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index d17b2d9a9c7..a25a3f18903 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -227,6 +227,8 @@ export class AudioTrackController extends BasePlaylistController { // (undocumented) destroy(): void; // (undocumented) + protected loadingPlaylist(audioTrack: MediaPlaylist, hlsUrlParameters: HlsUrlParameters | undefined): void; + // (undocumented) protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void; // (undocumented) protected onAudioTrackLoaded(event: Events.AUDIO_TRACK_LOADED, data: AudioTrackLoadedData): void; @@ -303,33 +305,29 @@ export type BaseData = { export class BasePlaylistController extends Logger implements NetworkComponentAPI { constructor(hls: Hls, logPrefix: string); // (undocumented) - protected canLoad: boolean; - // (undocumented) protected checkRetry(errorEvent: ErrorData): boolean; // (undocumented) - protected clearTimer(): void; - // (undocumented) destroy(): void; // (undocumented) + protected getUrlWithDirectives(uri: string, hlsUrlParameters: HlsUrlParameters | undefined): string; + // (undocumented) protected hls: Hls; // (undocumented) + protected loadingPlaylist(playlist: Level | MediaPlaylist, hlsUrlParameters?: HlsUrlParameters): void; + // (undocumented) protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void; // (undocumented) protected playlistLoaded(index: number, data: LevelLoadedData | AudioTrackLoadedData | TrackLoadedData, previousDetails?: LevelDetails): void; // (undocumented) - protected requestScheduled: number; + protected scheduleLoading(levelOrTrack: Level | MediaPlaylist, deliveryDirectives?: HlsUrlParameters): void; // (undocumented) - protected shouldLoadPlaylist(playlist: Level | MediaPlaylist | null | undefined): boolean; - // (undocumented) - protected shouldReloadPlaylist(playlist: Level | MediaPlaylist | null | undefined): boolean; + protected shouldLoadPlaylist(playlist: Level | MediaPlaylist | null | undefined): playlist is Level | MediaPlaylist; // (undocumented) startLoad(): void; // (undocumented) stopLoad(): void; // (undocumented) protected switchParams(playlistUri: string, previous: LevelDetails | undefined, current: LevelDetails | undefined): HlsUrlParameters | undefined; - // (undocumented) - protected timer: number; } // Warning: (ae-missing-release-tag) "BaseSegment" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -3182,6 +3180,8 @@ export class LevelDetails { // (undocumented) renditionReports?: AttrList[]; // (undocumented) + requestScheduled: number; + // (undocumented) skippedSegments: number; // (undocumented) startCC: number; @@ -4482,6 +4482,8 @@ export class SubtitleTrackController extends BasePlaylistController { // (undocumented) destroy(): void; // (undocumented) + protected loadingPlaylist(currentTrack: MediaPlaylist, hlsUrlParameters: HlsUrlParameters | undefined): void; + // (undocumented) protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void; // (undocumented) protected onError(event: Events.ERROR, data: ErrorData): void; diff --git a/src/controller/audio-track-controller.ts b/src/controller/audio-track-controller.ts index d5767576ef6..6804bcf962e 100644 --- a/src/controller/audio-track-controller.ts +++ b/src/controller/audio-track-controller.ts @@ -208,9 +208,6 @@ class AudioTrackController extends BasePlaylistController { error, }); } - } else if (this.shouldReloadPlaylist(currentTrack)) { - // Retry playlist loading if no playlist is or has been loaded yet - this.setAudioTrack(this.trackId); } } @@ -224,7 +221,6 @@ class AudioTrackController extends BasePlaylistController { data.context.id === this.trackId && (!this.groupIds || this.groupIds.indexOf(data.context.groupId) !== -1) ) { - this.requestScheduled = -1; this.checkRetry(data); } } @@ -319,9 +315,6 @@ class AudioTrackController extends BasePlaylistController { return; } - // stopping live reloading timer if any - this.clearTimer(); - this.selectDefaultTrack = false; const lastTrack = this.currentTrack; const track = tracks[newId]; @@ -403,49 +396,42 @@ class AudioTrackController extends BasePlaylistController { } protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void { + super.loadPlaylist(); const audioTrack = this.currentTrack; - if (!audioTrack) { + if (!this.shouldLoadPlaylist(audioTrack)) { return; } - let url = audioTrack.url; - if ( - this.shouldLoadPlaylist(audioTrack) && - url !== this.hls.levels[this.hls.loadLevel]?.uri - ) { - super.loadPlaylist(); - const id = audioTrack.id; - const groupId = audioTrack.groupId as string; - if (hlsUrlParameters) { - try { - url = hlsUrlParameters.addDirectives(url); - } catch (error) { - this.warn( - `Could not construct new URL with HLS Delivery Directives: ${error}`, - ); - } - } - // track not retrieved yet, or live playlist we need to (re)load it - const details = audioTrack.details; - const age = details?.age; - this.log( - `Loading audio-track ${id} "${audioTrack.name}" lang:${audioTrack.lang} group:${groupId}${ - hlsUrlParameters?.msn !== undefined - ? ' at sn ' + - hlsUrlParameters.msn + - ' part ' + - hlsUrlParameters.part - : '' - }${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type || '' : '') : ''} ${url}`, - ); - this.clearTimer(); - this.hls.trigger(Events.AUDIO_TRACK_LOADING, { - url, - id, - groupId, - deliveryDirectives: hlsUrlParameters || null, - track: audioTrack, - }); + if (audioTrack.url === this.hls.levels[this.hls.loadLevel]?.uri) { + // Do not load audio rendition with URI matching main variant URI + return; } + this.scheduleLoading(audioTrack, hlsUrlParameters); + } + + protected loadingPlaylist( + audioTrack: MediaPlaylist, + hlsUrlParameters: HlsUrlParameters | undefined, + ) { + super.loadingPlaylist(audioTrack, hlsUrlParameters); + const id = audioTrack.id; + const groupId = audioTrack.groupId as string; + const url = this.getUrlWithDirectives(audioTrack.url, hlsUrlParameters); + const details = audioTrack.details; + const age = details?.age; + this.log( + `Loading audio-track ${id} "${audioTrack.name}" lang:${audioTrack.lang} group:${groupId}${ + hlsUrlParameters?.msn !== undefined + ? ' at sn ' + hlsUrlParameters.msn + ' part ' + hlsUrlParameters.part + : '' + }${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type || '' : '') : ''} ${url}`, + ); + this.hls.trigger(Events.AUDIO_TRACK_LOADING, { + url, + id, + groupId, + deliveryDirectives: hlsUrlParameters || null, + track: audioTrack, + }); } } diff --git a/src/controller/base-playlist-controller.ts b/src/controller/base-playlist-controller.ts index 7f7f34eb0ae..f2723adbb24 100644 --- a/src/controller/base-playlist-controller.ts +++ b/src/controller/base-playlist-controller.ts @@ -24,35 +24,33 @@ export default class BasePlaylistController implements NetworkComponentAPI { protected hls: Hls; - protected timer: number = -1; - protected requestScheduled: number = -1; - protected canLoad: boolean = false; + private timer: number = -1; + private canLoad: boolean = false; constructor(hls: Hls, logPrefix: string) { super(logPrefix, hls.logger); this.hls = hls; } - public destroy(): void { + public destroy() { this.clearTimer(); // @ts-ignore this.hls = this.log = this.warn = null; } - protected clearTimer(): void { + private clearTimer() { if (this.timer !== -1) { self.clearTimeout(this.timer); this.timer = -1; } } - public startLoad(): void { + public startLoad() { this.canLoad = true; - this.requestScheduled = -1; this.loadPlaylist(); } - public stopLoad(): void { + public stopLoad() { this.canLoad = false; this.clearTimer(); } @@ -104,16 +102,22 @@ export default class BasePlaylistController } } - protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void { - if (this.requestScheduled === -1) { - this.requestScheduled = self.performance.now(); - } + protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters) { + // Loading is handled by the subclasses + this.clearTimer(); + } + + protected loadingPlaylist( + playlist: Level | MediaPlaylist, + hlsUrlParameters?: HlsUrlParameters, + ) { // Loading is handled by the subclasses + this.clearTimer(); } protected shouldLoadPlaylist( playlist: Level | MediaPlaylist | null | undefined, - ): boolean { + ): playlist is Level | MediaPlaylist { return ( this.canLoad && !!playlist && @@ -122,14 +126,20 @@ export default class BasePlaylistController ); } - protected shouldReloadPlaylist( - playlist: Level | MediaPlaylist | null | undefined, - ): boolean { - return ( - this.timer === -1 && - this.requestScheduled === -1 && - this.shouldLoadPlaylist(playlist) - ); + protected getUrlWithDirectives( + uri: string, + hlsUrlParameters: HlsUrlParameters | undefined, + ): string { + if (hlsUrlParameters) { + try { + return hlsUrlParameters.addDirectives(uri); + } catch (error) { + this.warn( + `Could not construct new URL with HLS Delivery Directives: ${error}`, + ); + } + } + return uri; } protected playlistLoaded( @@ -158,18 +168,17 @@ export default class BasePlaylistController // if current playlist is a live playlist, arm a timer to reload it if (details.live || previousDetails?.live) { + const levelOrTrack = 'levelInfo' in data ? data.levelInfo : data.track; details.reloaded(previousDetails); - if (previousDetails) { - this.log( - `live playlist ${index} ${ - details.advanced - ? 'REFRESHED ' + details.lastPartSn + '-' + details.lastPartIndex - : details.updated - ? 'UPDATED' - : 'MISSED' - }`, - ); - } + this.log( + `live playlist ${index} ${ + details.advanced + ? 'REFRESHED ' + details.lastPartSn + '-' + details.lastPartIndex + : details.updated + ? 'UPDATED' + : 'MISSED' + }`, + ); // Merge live playlists to adjust fragment starts and fill in delta playlist skipped segments if (previousDetails && details.fragments.length > 0) { mergeDetails(previousDetails, details); @@ -250,7 +259,7 @@ export default class BasePlaylistController part, ); if (lowLatencyMode || !lastPart) { - this.loadPlaylist(deliveryDirectives); + this.loadingPlaylist(levelOrTrack, deliveryDirectives); return; } } else if (details.canBlockReload || details.canSkipUntil) { @@ -261,6 +270,12 @@ export default class BasePlaylistController part, ); } + if (details.requestScheduled === -1) { + details.requestScheduled = stats.loading.start; + } + if (deliveryDirectives && msn !== undefined && details.canBlockReload) { + details.requestScheduled -= details.partTarget * 1000 || 1000; + } const bufferInfo = this.hls.mainForwardBufferInfo; const position = bufferInfo ? bufferInfo.end - bufferInfo.len : 0; const distanceToLiveEdgeMs = (details.edge - position) * 1000; @@ -268,55 +283,61 @@ export default class BasePlaylistController details, distanceToLiveEdgeMs, ); - if (details.updated && now > this.requestScheduled + reloadInterval) { - this.requestScheduled = stats.loading.start; - } - - if (msn !== undefined && details.canBlockReload) { - this.requestScheduled = - stats.loading.first + - reloadInterval - - (details.partTarget * 1000 || 1000); - } else if ( - this.requestScheduled === -1 || - this.requestScheduled + reloadInterval < now - ) { - this.requestScheduled = now; - } else if (this.requestScheduled - now <= 0) { - this.requestScheduled += reloadInterval; + if (details.requestScheduled + reloadInterval < now) { + details.requestScheduled = now; + } else { + details.requestScheduled += reloadInterval; } - let estimatedTimeUntilUpdate = this.requestScheduled - now; - estimatedTimeUntilUpdate = Math.max(0, estimatedTimeUntilUpdate); - this.log( - `reload live playlist ${index} in ${Math.round( - estimatedTimeUntilUpdate, - )} ms`, - ); - // this.log( - // `live reload ${details.updated ? 'REFRESHED' : 'MISSED'} - // reload in ${estimatedTimeUntilUpdate / 1000} - // round trip ${(stats.loading.end - stats.loading.start) / 1000} - // diff ${ - // (reloadInterval - - // (estimatedTimeUntilUpdate + - // stats.loading.end - - // stats.loading.start)) / - // 1000 - // } - // reload interval ${reloadInterval / 1000} - // target duration ${details.targetduration} - // distance to edge ${distanceToLiveEdgeMs / 1000}` - // ); - - this.timer = self.setTimeout( - () => this.loadPlaylist(deliveryDirectives), - estimatedTimeUntilUpdate, - ); + this.scheduleLoading(levelOrTrack, deliveryDirectives); } else { this.clearTimer(); } } + protected scheduleLoading( + levelOrTrack: Level | MediaPlaylist, + deliveryDirectives?: HlsUrlParameters, + ) { + const details = levelOrTrack.details; + if (!details) { + this.loadingPlaylist(levelOrTrack, deliveryDirectives); + return; + } + const now = self.performance.now(); + const requestScheduled = details.requestScheduled; + if (now >= requestScheduled) { + this.loadingPlaylist(levelOrTrack, deliveryDirectives); + return; + } + + const estimatedTimeUntilUpdate = requestScheduled - now; + this.log( + `reload live playlist ${levelOrTrack.name || levelOrTrack.bitrate + 'bps'} in ${Math.round( + estimatedTimeUntilUpdate, + )} ms`, + ); + // this.log( + // `live reload ${details.updated ? 'REFRESHED' : 'MISSED'} + // reload in ${estimatedTimeUntilUpdate / 1000} + // round trip ${(stats.loading.end - stats.loading.start) / 1000} + // diff ${ + // (reloadInterval - + // (estimatedTimeUntilUpdate + + // stats.loading.end - + // stats.loading.start)) / + // 1000 + // } + // reload interval ${reloadInterval / 1000} + // target duration ${details.targetduration} + // distance to edge ${distanceToLiveEdgeMs / 1000}` + // ); + + this.timer = self.setTimeout( + () => this.loadingPlaylist(levelOrTrack, deliveryDirectives), + estimatedTimeUntilUpdate, + ); + } + private getDeliveryDirectives( details: LevelDetails, previousDeliveryDirectives: HlsUrlParameters | null, @@ -344,7 +365,6 @@ export default class BasePlaylistController (!errorAction.resolved && action === NetworkErrorAction.SendAlternateToPenaltyBox)); if (retry) { - this.requestScheduled = -1; if (retryCount >= retryConfig.maxNumRetry) { return false; } diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 782f8cbfc33..7f2b2b8441f 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -1663,7 +1663,7 @@ export default class BaseStreamController const { media } = this; // if we have not yet loaded any fragment, start loading from start position let pos = 0; - if (this.hls.hasEnoughToStart && media) { + if (this.hls?.hasEnoughToStart && media) { pos = media.currentTime; } else if (this.nextLoadPosition >= 0) { pos = this.nextLoadPosition; diff --git a/src/controller/level-controller.ts b/src/controller/level-controller.ts index 2d7772eb1e8..55b799d5a56 100644 --- a/src/controller/level-controller.ts +++ b/src/controller/level-controller.ts @@ -442,9 +442,7 @@ export default class LevelController extends BasePlaylistController { lastLevel && lastPathwayId === pathwayId ) { - if (level.details || this.requestScheduled !== -1) { - return; - } + return; } this.log( @@ -572,10 +570,7 @@ export default class LevelController extends BasePlaylistController { data.context.type === PlaylistContextType.LEVEL && data.context.level === this.level ) { - const retry = this.checkRetry(data); - if (!retry) { - this.requestScheduled = -1; - } + this.checkRetry(data); } } @@ -632,47 +627,37 @@ export default class LevelController extends BasePlaylistController { protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters) { super.loadPlaylist(); - const currentLevelIndex = this.currentLevelIndex; - const currentLevel = this.currentLevel; - - if (currentLevel && this.shouldLoadPlaylist(currentLevel)) { - let url = currentLevel.uri; - if (hlsUrlParameters) { - try { - url = hlsUrlParameters.addDirectives(url); - } catch (error) { - this.warn( - `Could not construct new URL with HLS Delivery Directives: ${error}`, - ); - } - } + if (this.shouldLoadPlaylist(this.currentLevel)) { + this.scheduleLoading(this.currentLevel, hlsUrlParameters); + } + } - const pathwayId = currentLevel.attrs['PATHWAY-ID']; - const details = currentLevel.details; - const age = details?.age; - this.log( - `Loading level index ${currentLevelIndex}${ - hlsUrlParameters?.msn !== undefined - ? ' at sn ' + - hlsUrlParameters.msn + - ' part ' + - hlsUrlParameters.part - : '' - }${pathwayId ? ' Pathway ' + pathwayId : ''}${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type || '' : '') : ''} ${url}`, - ); + protected loadingPlaylist( + currentLevel: Level, + hlsUrlParameters: HlsUrlParameters | undefined, + ) { + super.loadingPlaylist(currentLevel, hlsUrlParameters); + const url = this.getUrlWithDirectives(currentLevel.uri, hlsUrlParameters); + const currentLevelIndex = this.currentLevelIndex; + const pathwayId = currentLevel.attrs['PATHWAY-ID']; + const details = currentLevel.details; + const age = details?.age; + this.log( + `Loading level index ${currentLevelIndex}${ + hlsUrlParameters?.msn !== undefined + ? ' at sn ' + hlsUrlParameters.msn + ' part ' + hlsUrlParameters.part + : '' + }${pathwayId ? ' Pathway ' + pathwayId : ''}${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type || '' : '') : ''} ${url}`, + ); - // console.log('Current audio track group ID:', this.hls.audioTracks[this.hls.audioTrack].groupId); - // console.log('New video quality level audio group id:', levelObject.attrs.AUDIO, level); - this.clearTimer(); - this.hls.trigger(Events.LEVEL_LOADING, { - url, - level: currentLevelIndex, - levelInfo: currentLevel, - pathwayId: currentLevel.attrs['PATHWAY-ID'], - id: 0, // Deprecated Level urlId - deliveryDirectives: hlsUrlParameters || null, - }); - } + this.hls.trigger(Events.LEVEL_LOADING, { + url, + level: currentLevelIndex, + levelInfo: currentLevel, + pathwayId: currentLevel.attrs['PATHWAY-ID'], + id: 0, // Deprecated Level urlId + deliveryDirectives: hlsUrlParameters || null, + }); } get nextLoadLevel() { diff --git a/src/controller/subtitle-track-controller.ts b/src/controller/subtitle-track-controller.ts index f120d7a0d7e..976961dd98c 100644 --- a/src/controller/subtitle-track-controller.ts +++ b/src/controller/subtitle-track-controller.ts @@ -292,9 +292,6 @@ class SubtitleTrackController extends BasePlaylistController { if (trackId !== -1 && this.trackId === -1) { this.setSubtitleTrack(trackId); } - } else if (this.shouldReloadPlaylist(currentTrack)) { - // Retry playlist loading if no playlist is or has been loaded yet - this.setSubtitleTrack(this.trackId); } } @@ -433,42 +430,37 @@ class SubtitleTrackController extends BasePlaylistController { protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void { super.loadPlaylist(); - const currentTrack = this.currentTrack; - if (this.shouldLoadPlaylist(currentTrack) && currentTrack) { - const id = currentTrack.id; - const groupId = currentTrack.groupId as string; - let url = currentTrack.url; - if (hlsUrlParameters) { - try { - url = hlsUrlParameters.addDirectives(url); - } catch (error) { - this.warn( - `Could not construct new URL with HLS Delivery Directives: ${error}`, - ); - } - } - const details = currentTrack.details; - const age = details?.age; - this.log( - `Loading subtitle ${id} "${currentTrack.name}" lang:${currentTrack.lang} group:${groupId}${ - hlsUrlParameters?.msn !== undefined - ? ' at sn ' + - hlsUrlParameters.msn + - ' part ' + - hlsUrlParameters.part - : '' - }${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type || '' : '') : ''} ${url}`, - ); - this.hls.trigger(Events.SUBTITLE_TRACK_LOADING, { - url, - id, - groupId, - deliveryDirectives: hlsUrlParameters || null, - track: currentTrack, - }); + if (this.shouldLoadPlaylist(this.currentTrack)) { + this.scheduleLoading(this.currentTrack, hlsUrlParameters); } } + protected loadingPlaylist( + currentTrack: MediaPlaylist, + hlsUrlParameters: HlsUrlParameters | undefined, + ) { + super.loadingPlaylist(currentTrack, hlsUrlParameters); + const id = currentTrack.id; + const groupId = currentTrack.groupId as string; + const url = this.getUrlWithDirectives(currentTrack.url, hlsUrlParameters); + const details = currentTrack.details; + const age = details?.age; + this.log( + `Loading subtitle ${id} "${currentTrack.name}" lang:${currentTrack.lang} group:${groupId}${ + hlsUrlParameters?.msn !== undefined + ? ' at sn ' + hlsUrlParameters.msn + ' part ' + hlsUrlParameters.part + : '' + }${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type || '' : '') : ''} ${url}`, + ); + this.hls.trigger(Events.SUBTITLE_TRACK_LOADING, { + url, + id, + groupId, + deliveryDirectives: hlsUrlParameters || null, + track: currentTrack, + }); + } + /** * Disables the old subtitleTrack and sets current mode on the next subtitleTrack. * This operates on the DOM textTracks. @@ -528,9 +520,6 @@ class SubtitleTrackController extends BasePlaylistController { return; } - // stopping live reloading timer if any - this.clearTimer(); - this.selectDefaultTrack = false; const lastTrack = this.currentTrack; const track: MediaPlaylist | null = tracks[newId] || null; diff --git a/src/loader/level-details.ts b/src/loader/level-details.ts index 34c3e554fce..c363f27c9cb 100644 --- a/src/loader/level-details.ts +++ b/src/loader/level-details.ts @@ -20,6 +20,7 @@ export class LevelDetails { public dateRanges: Record; public dateRangeTagCount: number = 0; public live: boolean = true; + public requestScheduled: number = -1; public ageHeader: number = 0; public advancedDateTime?: number; public updated: boolean = true; diff --git a/src/utils/level-helper.ts b/src/utils/level-helper.ts index b40d4f4f7f3..cba4e78b3c0 100644 --- a/src/utils/level-helper.ts +++ b/src/utils/level-helper.ts @@ -315,6 +315,9 @@ export function mergeDetails( newDetails.driftEnd = oldDetails.driftEnd; newDetails.advancedDateTime = oldDetails.advancedDateTime; } + if (newDetails.requestScheduled === -1) { + newDetails.requestScheduled = oldDetails.requestScheduled; + } } function mergeDateRanges( diff --git a/tests/unit/controller/subtitle-track-controller.ts b/tests/unit/controller/subtitle-track-controller.ts index 0a391e1ea8a..3d08399f617 100644 --- a/tests/unit/controller/subtitle-track-controller.ts +++ b/tests/unit/controller/subtitle-track-controller.ts @@ -420,9 +420,13 @@ describe('SubtitleTrackController', function () { ); }); - it('should trigger SUBTITLE_TRACK_LOADING if the track is live, even if it has details', function () { + it('should trigger SUBTITLE_TRACK_LOADING if the track is live and needs to be reloaded', function () { const triggerSpy = sandbox.spy(hls, 'trigger'); - subtitleTracks[2].details = { live: true } as any; + subtitleTracks[2].details = { + live: true, + requestScheduled: -100000, + targetduration: 2, + } as any; subtitleTrackController.startLoad(); subtitleTrackController.subtitleTrack = 2;