From db37e2dba3ae64edfe50571a62644ac24aaec207 Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Fri, 15 Nov 2024 12:21:07 -0800 Subject: [PATCH] Force media capabilities check for HEVC to avoid failure on Windows Fix removeLevel side-effects when removing current level or changing index of loading level Failed test results: https://github.com/video-dev/hls.js/actions/runs/11844914912/job/33012913783?pr=6845 --- api-extractor/report/hls.js.api.md | 10 ++++ src/controller/abr-controller.ts | 8 +++- src/controller/audio-track-controller.ts | 1 + src/controller/level-controller.ts | 5 +- src/controller/stream-controller.ts | 4 +- src/controller/subtitle-track-controller.ts | 1 + src/loader/playlist-loader.ts | 46 +++++++++++++++---- src/types/events.ts | 4 ++ src/types/loader.ts | 5 +- .../controller/audio-stream-controller.ts | 1 + .../unit/controller/audio-track-controller.ts | 2 + tests/unit/controller/stream-controller.ts | 12 +++++ .../controller/subtitle-track-controller.ts | 8 ++++ 13 files changed, 92 insertions(+), 15 deletions(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index fbded3802e2..92b91d5117d 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -3237,6 +3237,8 @@ export interface LevelLoadedData { // (undocumented) level: number; // (undocumented) + levelInfo: Level; + // (undocumented) networkDetails: any; // (undocumented) stats: LoaderStats; @@ -3253,6 +3255,8 @@ export interface LevelLoadingData { // (undocumented) level: number; // (undocumented) + levelInfo: Level; + // (undocumented) pathwayId: string | undefined; // (undocumented) url: string; @@ -4141,6 +4145,8 @@ export interface PlaylistLoaderContext extends LoaderContext { // (undocumented) levelDetails?: LevelDetails; // (undocumented) + levelOrTrack: Level | MediaPlaylist | null; + // (undocumented) pathwayId?: string; // (undocumented) type: PlaylistContextType; @@ -4626,6 +4632,8 @@ export interface TrackLoadedData { networkDetails: any; // (undocumented) stats: LoaderStats; + // (undocumented) + track: MediaPlaylist; } // Warning: (ae-missing-release-tag) "TrackLoadingData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -4639,6 +4647,8 @@ export interface TrackLoadingData { // (undocumented) id: number; // (undocumented) + track: MediaPlaylist; + // (undocumented) url: string; } diff --git a/src/controller/abr-controller.ts b/src/controller/abr-controller.ts index d3a69b42ebc..d917464aaa9 100644 --- a/src/controller/abr-controller.ts +++ b/src/controller/abr-controller.ts @@ -794,14 +794,15 @@ class AbrController extends Logger implements AbrComponentAPI { | undefined; if ( typeof mediaCapabilities?.decodingInfo === 'function' && - requiresMediaCapabilitiesDecodingInfo( + (requiresMediaCapabilitiesDecodingInfo( levelInfo, audioTracksByGroup, currentVideoRange, currentFrameRate, currentBw, audioPreference, - ) + ) || + levelInfo.videoCodec?.substring(0, 4) === 'hvc1') // Force media capabilities check for HEVC to avoid failure on Windows ) { levelInfo.supportedPromise = getMediaDecodingInfoPromise( levelInfo, @@ -830,6 +831,9 @@ class AbrController extends Logger implements AbrComponentAPI { if (index > -1 && levels.length > 1) { this.log(`Removing unsupported level ${index}`); this.hls.removeLevel(index); + if (this.hls.loadLevel === -1) { + this.hls.nextLoadLevel = 0; + } } } }); diff --git a/src/controller/audio-track-controller.ts b/src/controller/audio-track-controller.ts index 079ef312c3c..386fedb8c0c 100644 --- a/src/controller/audio-track-controller.ts +++ b/src/controller/audio-track-controller.ts @@ -428,6 +428,7 @@ class AudioTrackController extends BasePlaylistController { id, groupId, deliveryDirectives: hlsUrlParameters || null, + track: audioTrack, }); } } diff --git a/src/controller/level-controller.ts b/src/controller/level-controller.ts index 4534b5360c9..55b2695a001 100644 --- a/src/controller/level-controller.ts +++ b/src/controller/level-controller.ts @@ -601,7 +601,7 @@ export default class LevelController extends BasePlaylistController { protected onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) { const { level, details } = data; - const curLevel = this._levels[level]; + const curLevel = data.levelInfo; if (!curLevel) { this.warn(`Invalid level index ${level}`); @@ -612,7 +612,7 @@ export default class LevelController extends BasePlaylistController { } // only process level loaded events matching with expected level - if (level === this.currentLevelIndex) { + if (curLevel === this.currentLevel) { // reset level load error counter on successful level loaded only if there is no issues with fragments if (curLevel.fragmentError === 0) { curLevel.loadError = 0; @@ -665,6 +665,7 @@ export default class LevelController extends BasePlaylistController { this.hls.trigger(Events.LEVEL_LOADING, { url, level: currentLevelIndex, + levelInfo: currentLevel, pathwayId: currentLevel.attrs['PATHWAY-ID'], id: 0, // Deprecated Level urlId deliveryDirectives: hlsUrlParameters || null, diff --git a/src/controller/stream-controller.ts b/src/controller/stream-controller.ts index 638b8f921b1..d73df2d0b58 100644 --- a/src/controller/stream-controller.ts +++ b/src/controller/stream-controller.ts @@ -639,7 +639,7 @@ export default class StreamController if (!levels || this.state !== State.IDLE) { return; } - const level = levels[data.level]; + const level = data.levelInfo; if ( !level.details || (level.details.live && this.levelLastLoaded !== level) || @@ -667,7 +667,7 @@ export default class StreamController }, cc [${newDetails.startCC}, ${newDetails.endCC}] duration:${duration}`, ); - const curLevel = levels[newLevelId]; + const curLevel = data.levelInfo; const fragCurrent = this.fragCurrent; if ( fragCurrent && diff --git a/src/controller/subtitle-track-controller.ts b/src/controller/subtitle-track-controller.ts index 9b93a661167..d0e402f5936 100644 --- a/src/controller/subtitle-track-controller.ts +++ b/src/controller/subtitle-track-controller.ts @@ -453,6 +453,7 @@ class SubtitleTrackController extends BasePlaylistController { id, groupId, deliveryDirectives: hlsUrlParameters || null, + track: currentTrack, }); } } diff --git a/src/loader/playlist-loader.ts b/src/loader/playlist-loader.ts index 64a9ffe9148..e62a5c38329 100644 --- a/src/loader/playlist-loader.ts +++ b/src/loader/playlist-loader.ts @@ -18,10 +18,11 @@ import type { NetworkComponentAPI } from '../types/component-api'; import type { ErrorData, LevelLoadingData, + LevelsUpdatedData, ManifestLoadingData, TrackLoadingData, } from '../types/events'; -import type { LevelParsed, VariableMap } from '../types/level'; +import type { Level, LevelParsed, VariableMap } from '../types/level'; import type { Loader, LoaderCallbacks, @@ -31,7 +32,7 @@ import type { LoaderStats, PlaylistLoaderContext, } from '../types/loader'; -import type { MediaAttributes } from '../types/media-playlist'; +import type { MediaAttributes, MediaPlaylist } from '../types/media-playlist'; function mapContextToLevelType( context: PlaylistLoaderContext, @@ -86,6 +87,7 @@ class PlaylistLoader implements NetworkComponentAPI { hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this); hls.on(Events.AUDIO_TRACK_LOADING, this.onAudioTrackLoading, this); hls.on(Events.SUBTITLE_TRACK_LOADING, this.onSubtitleTrackLoading, this); + hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); } private unregisterListeners() { @@ -94,6 +96,7 @@ class PlaylistLoader implements NetworkComponentAPI { hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this); hls.off(Events.AUDIO_TRACK_LOADING, this.onAudioTrackLoading, this); hls.off(Events.SUBTITLE_TRACK_LOADING, this.onSubtitleTrackLoading, this); + hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); } /** @@ -118,7 +121,7 @@ class PlaylistLoader implements NetworkComponentAPI { return this.loaders[context.type]; } - private resetInternalLoader(contextType): void { + private resetInternalLoader(contextType: PlaylistContextType): void { if (this.loaders[contextType]) { delete this.loaders[contextType]; } @@ -134,7 +137,7 @@ class PlaylistLoader implements NetworkComponentAPI { loader.destroy(); } - this.resetInternalLoader(contextType); + this.resetInternalLoader(contextType as PlaylistContextType); } } @@ -157,11 +160,12 @@ class PlaylistLoader implements NetworkComponentAPI { type: PlaylistContextType.MANIFEST, url, deliveryDirectives: null, + levelOrTrack: null, }); } private onLevelLoading(event: Events.LEVEL_LOADING, data: LevelLoadingData) { - const { id, level, pathwayId, url, deliveryDirectives } = data; + const { id, level, pathwayId, url, deliveryDirectives, levelInfo } = data; this.load({ id, level, @@ -170,6 +174,7 @@ class PlaylistLoader implements NetworkComponentAPI { type: PlaylistContextType.LEVEL, url, deliveryDirectives, + levelOrTrack: levelInfo, }); } @@ -177,7 +182,7 @@ class PlaylistLoader implements NetworkComponentAPI { event: Events.AUDIO_TRACK_LOADING, data: TrackLoadingData, ) { - const { id, groupId, url, deliveryDirectives } = data; + const { id, groupId, url, deliveryDirectives, track } = data; this.load({ id, groupId, @@ -186,6 +191,7 @@ class PlaylistLoader implements NetworkComponentAPI { type: PlaylistContextType.AUDIO_TRACK, url, deliveryDirectives, + levelOrTrack: track, }); } @@ -193,7 +199,7 @@ class PlaylistLoader implements NetworkComponentAPI { event: Events.SUBTITLE_TRACK_LOADING, data: TrackLoadingData, ) { - const { id, groupId, url, deliveryDirectives } = data; + const { id, groupId, url, deliveryDirectives, track } = data; this.load({ id, groupId, @@ -202,9 +208,30 @@ class PlaylistLoader implements NetworkComponentAPI { type: PlaylistContextType.SUBTITLE_TRACK, url, deliveryDirectives, + levelOrTrack: track, }); } + private onLevelsUpdated( + event: Events.LEVELS_UPDATED, + data: LevelsUpdatedData, + ) { + // abort and delete loader of removed levels + const loader = this.loaders[PlaylistContextType.LEVEL]; + if (loader) { + const context = loader.context; + if ( + context && + !data.levels.some( + (lvl) => lvl === (context as PlaylistLoaderContext).levelOrTrack, + ) + ) { + loader.abort(); + delete this.loaders[PlaylistContextType.LEVEL]; + } + } + } + private load(context: PlaylistLoaderContext): void { const config = this.hls.config; @@ -217,7 +244,7 @@ class PlaylistLoader implements NetworkComponentAPI { if ( loaderContext && loaderContext.url === context.url && - loaderContext.level === context.level + loaderContext.levelOrTrack === context.levelOrTrack ) { // same URL can't overlap this.hls.logger.trace('[playlist-loader]: playlist request ongoing'); @@ -680,6 +707,7 @@ class PlaylistLoader implements NetworkComponentAPI { case PlaylistContextType.LEVEL: hls.trigger(Events.LEVEL_LOADED, { details: levelDetails, + levelInfo: (context.levelOrTrack as Level) || hls.levels[0], level: levelIndex || 0, id: id || 0, stats, @@ -690,6 +718,7 @@ class PlaylistLoader implements NetworkComponentAPI { case PlaylistContextType.AUDIO_TRACK: hls.trigger(Events.AUDIO_TRACK_LOADED, { details: levelDetails, + track: context.levelOrTrack as MediaPlaylist, id: id || 0, groupId: groupId || '', stats, @@ -700,6 +729,7 @@ class PlaylistLoader implements NetworkComponentAPI { case PlaylistContextType.SUBTITLE_TRACK: hls.trigger(Events.SUBTITLE_TRACK_LOADED, { details: levelDetails, + track: context.levelOrTrack as MediaPlaylist, id: id || 0, groupId: groupId || '', stats, diff --git a/src/types/events.ts b/src/types/events.ts index e83d665c49d..ad1c7e39bbe 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -188,6 +188,7 @@ export interface LevelSwitchedData { export interface TrackLoadingData { id: number; groupId: string; + track: MediaPlaylist; url: string; deliveryDirectives: HlsUrlParameters | null; } @@ -195,6 +196,7 @@ export interface TrackLoadingData { export interface LevelLoadingData { id: number; level: number; + levelInfo: Level; pathwayId: string | undefined; url: string; deliveryDirectives: HlsUrlParameters | null; @@ -207,12 +209,14 @@ export interface TrackLoadedData { networkDetails: any; stats: LoaderStats; deliveryDirectives: HlsUrlParameters | null; + track: MediaPlaylist; } export interface LevelLoadedData { details: LevelDetails; id: number; level: number; + levelInfo: Level; networkDetails: any; stats: LoaderStats; deliveryDirectives: HlsUrlParameters | null; diff --git a/src/types/loader.ts b/src/types/loader.ts index d2112f93a5e..8040e214aa6 100644 --- a/src/types/loader.ts +++ b/src/types/loader.ts @@ -1,5 +1,6 @@ import type { LoaderConfig } from '../config'; -import type { HlsUrlParameters } from './level'; +import type { HlsUrlParameters, Level } from './level'; +import type { MediaPlaylist } from './media-playlist'; import type { Fragment } from '../loader/fragment'; import type { Part } from '../loader/fragment'; import type { KeyLoaderInfo } from '../loader/key-loader'; @@ -191,4 +192,6 @@ export interface PlaylistLoaderContext extends LoaderContext { levelDetails?: LevelDetails; // Blocking playlist request delivery directives (or null id none were added to playlist url deliveryDirectives: HlsUrlParameters | null; + // Reference to level or track object in hls.levels, hls.allAudioTracks, or hls.allSubtitleTracks (null when loading MVP) + levelOrTrack: Level | MediaPlaylist | null; } diff --git a/tests/unit/controller/audio-stream-controller.ts b/tests/unit/controller/audio-stream-controller.ts index 9b7aabee2c2..69ae711c803 100644 --- a/tests/unit/controller/audio-stream-controller.ts +++ b/tests/unit/controller/audio-stream-controller.ts @@ -182,6 +182,7 @@ describe('AudioStreamController', function () { }, targetduration: 100, } as unknown as LevelDetails, + track: {} as any, }; audioStreamController.levels = tracks; diff --git a/tests/unit/controller/audio-track-controller.ts b/tests/unit/controller/audio-track-controller.ts index dea8b94677f..cccbed0ce91 100644 --- a/tests/unit/controller/audio-track-controller.ts +++ b/tests/unit/controller/audio-track-controller.ts @@ -283,6 +283,7 @@ describe('AudioTrackController', function () { networkDetails: null, stats: { loading: {} } as any, deliveryDirectives: null, + track: {} as any, }); expect(audioTrackController.tracksInGroup[0], 'tracksInGroup[0]') .to.have.property('details') @@ -299,6 +300,7 @@ describe('AudioTrackController', function () { networkDetails: null, stats: { loading: {} } as any, deliveryDirectives: null, + track: {} as any, }); expect(audioTrackController.tracksInGroup[1], 'tracksInGroup[1]') .to.have.property('details') diff --git a/tests/unit/controller/stream-controller.ts b/tests/unit/controller/stream-controller.ts index e56f80b56fb..219df4b06ee 100644 --- a/tests/unit/controller/stream-controller.ts +++ b/tests/unit/controller/stream-controller.ts @@ -129,6 +129,12 @@ describe('StreamController', function () { networkDetails: {}, stats: new LoadStats(), deliveryDirectives: null, + levelInfo: new Level({ + name: '', + url: '', + attrs, + bitrate: 500000, + }), }); expect(streamController['startPosition']).to.equal(130.5); expect(streamController['nextLoadPosition']).to.equal(130.5); @@ -174,6 +180,12 @@ describe('StreamController', function () { networkDetails: {}, stats: new LoadStats(), deliveryDirectives: null, + levelInfo: new Level({ + name: '', + url: '', + attrs, + bitrate: 500000, + }), }); expect(streamController['startPosition']).to.equal(18); expect(streamController['nextLoadPosition']).to.equal(18); diff --git a/tests/unit/controller/subtitle-track-controller.ts b/tests/unit/controller/subtitle-track-controller.ts index 7864f5f3339..0a391e1ea8a 100644 --- a/tests/unit/controller/subtitle-track-controller.ts +++ b/tests/unit/controller/subtitle-track-controller.ts @@ -128,6 +128,7 @@ describe('SubtitleTrackController', function () { pathwayId: undefined, url: '', deliveryDirectives: null, + levelInfo: {} as any, }); }; @@ -386,6 +387,7 @@ describe('SubtitleTrackController', function () { id: 1, groupId: 'default-text-group', deliveryDirectives: null, + track: subtitleTrackController.subtitleTracks[1], }, ); }); @@ -432,6 +434,7 @@ describe('SubtitleTrackController', function () { id: 2, groupId: 'default-text-group', deliveryDirectives: null, + track: subtitleTrackController.subtitleTracks[2], }, ); }); @@ -489,6 +492,7 @@ describe('SubtitleTrackController', function () { stats: new LoadStats(), networkDetails: {}, deliveryDirectives: null, + track: {} as any, }; hls.trigger(Events.SUBTITLE_TRACK_LOADED, mockLoadedEvent); expect((subtitleTrackController as any).timer).to.equal(-1); @@ -521,6 +525,7 @@ describe('SubtitleTrackController', function () { stats: new LoadStats(), networkDetails: {}, deliveryDirectives: null, + track: {} as any, }; hls.subtitleTrack = -1; @@ -544,6 +549,7 @@ describe('SubtitleTrackController', function () { stats: new LoadStats(), networkDetails: {}, deliveryDirectives: null, + track: {} as any, }); expect((subtitleTrackController as any).timer).to.equal(-1); }); @@ -560,6 +566,7 @@ describe('SubtitleTrackController', function () { stats: new LoadStats(), networkDetails: {}, deliveryDirectives: null, + track: {} as any, }); expect((subtitleTrackController as any).timer).to.exist; }); @@ -577,6 +584,7 @@ describe('SubtitleTrackController', function () { stats: new LoadStats(), networkDetails: {}, deliveryDirectives: null, + track: {} as any, }); expect((subtitleTrackController as any).timer).to.equal(-1); });