From 7dc4bce28ab20390d319005faf767408051c7444 Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Tue, 23 Aug 2022 23:34:50 -0700 Subject: [PATCH] Parallelize key and segment requests --- api-extractor/report/hls.js.api.md | 10 + src/controller/abr-controller.ts | 4 +- src/controller/audio-stream-controller.ts | 12 +- src/controller/base-stream-controller.ts | 131 ++++++--- src/controller/stream-controller.ts | 26 +- src/controller/subtitle-stream-controller.ts | 16 +- src/demux/mp4demuxer.ts | 8 +- src/hls.ts | 4 +- src/loader/fragment-loader.ts | 6 +- src/loader/fragment.ts | 8 + src/loader/key-loader.ts | 266 ++++++++++--------- src/types/loader.ts | 2 + 12 files changed, 282 insertions(+), 211 deletions(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 636fe346a11..b6892e27e7b 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -724,6 +724,8 @@ export interface FragLoadingData { export class Fragment extends BaseSegment { constructor(type: PlaylistLevelType, baseurl: string); // (undocumented) + abortRequests(): void; + // (undocumented) appendedPTS?: number; // (undocumented) bitrateTest: boolean; @@ -753,6 +755,8 @@ export class Fragment extends BaseSegment { // (undocumented) initSegment: Fragment | null; // (undocumented) + keyLoader: Loader | null; + // (undocumented) level: number; // (undocumented) levelkey?: LevelKey; @@ -1213,6 +1217,12 @@ export interface KeyLoadedData { frag: Fragment; } +// Warning: (ae-missing-release-tag) "KeyLoaderContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface KeyLoaderContext extends FragmentLoaderContext { +} + // Warning: (ae-missing-release-tag) "KeyLoadingData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/controller/abr-controller.ts b/src/controller/abr-controller.ts index f6b64d93956..36c52f98e7a 100644 --- a/src/controller/abr-controller.ts +++ b/src/controller/abr-controller.ts @@ -198,9 +198,9 @@ class AbrController implements ComponentAPI { hls.nextLoadLevel = nextLoadLevel; this.bwEstimator.sample(requestDelay, stats.loaded); this.clearTimer(); - if (frag.loader) { + if (frag.loader || frag.keyLoader) { this.fragCurrent = this.partCurrent = null; - frag.loader.abort(); + frag.abortRequests(); } hls.trigger(Events.FRAG_LOAD_EMERGENCY_ABORTED, { frag, part, stats }); } diff --git a/src/controller/audio-stream-controller.ts b/src/controller/audio-stream-controller.ts index 8cab64dd003..e509809473e 100644 --- a/src/controller/audio-stream-controller.ts +++ b/src/controller/audio-stream-controller.ts @@ -363,11 +363,7 @@ class AudioStreamController return; } - if (frag.decryptdata?.keyFormat === 'identity' && !frag.decryptdata?.key) { - this.loadKey(frag, trackDetails); - } else { - this.loadFragment(frag, trackDetails, targetBufferTime); - } + this.loadFragment(frag, trackDetails, targetBufferTime); } protected getMaxBufferLength(mainBufferLength?: number): number { @@ -400,8 +396,8 @@ class AudioStreamController this.trackId = data.id; const { fragCurrent } = this; - if (fragCurrent?.loader) { - fragCurrent.loader.abort(); + if (fragCurrent) { + fragCurrent.abortRequests(); } this.fragCurrent = null; this.clearWaitingFragment(); @@ -827,7 +823,7 @@ class AudioStreamController fragState === FragmentState.PARTIAL ) { if (frag.sn === 'initSegment') { - this._loadInitSegment(frag); + this._loadInitSegment(frag, trackDetails); } else if (trackDetails.live && !Number.isFinite(this.initPTS[frag.cc])) { this.log( `Waiting for video PTS in continuity counter ${frag.cc} of live stream before loading audio fragment ${frag.sn} of level ${this.trackId}` diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 9caab4a0379..14de49b7c8e 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -23,6 +23,7 @@ import FragmentLoader, { FragmentLoadProgressCallback, LoadError, } from '../loader/fragment-loader'; +import KeyLoader from '../loader/key-loader'; import { LevelDetails } from '../loader/level-details'; import Decrypter from '../crypt/decrypter'; import TimeRanges from '../utils/time-ranges'; @@ -86,7 +87,8 @@ export default class BaseStreamController protected fragLoadError: number = 0; protected retryDate: number = 0; protected levels: Array | null = null; - protected fragmentLoader!: FragmentLoader; + protected fragmentLoader: FragmentLoader; + protected keyLoader: KeyLoader; protected levelLastLoaded: number | null = null; protected startFragRequested: boolean = false; protected decrypter: Decrypter; @@ -105,10 +107,10 @@ export default class BaseStreamController this.warn = logger.warn.bind(logger, `${logPrefix}:`); this.hls = hls; this.fragmentLoader = new FragmentLoader(hls.config); + this.keyLoader = new KeyLoader(hls.config); this.fragmentTracker = fragmentTracker; this.config = hls.config; this.decrypter = new Decrypter(hls as HlsEventEmitter, hls.config); - hls.on(Events.KEY_LOADED, this.onKeyLoaded, this); hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); } @@ -123,8 +125,10 @@ export default class BaseStreamController public stopLoad() { this.fragmentLoader.abort(); + this.keyLoader.abort(); const frag = this.fragCurrent; if (frag) { + frag.abortRequests(); this.fragmentTracker.removeFragment(frag); } this.resetTransmuxer(); @@ -238,7 +242,7 @@ export default class BaseStreamController this.log( 'seeking outside of buffer while fragment load in progress, cancel fragment load' ); - fragCurrent.loader.abort(); + fragCurrent.abortRequests(); } this.resetLoadingState(); } @@ -262,21 +266,6 @@ export default class BaseStreamController this.startPosition = this.lastCurrentTime = 0; } - onKeyLoaded(event: Events.KEY_LOADED, data: KeyLoadedData) { - if ( - this.state !== State.KEY_LOADING || - data.frag !== this.fragCurrent || - !this.levels - ) { - return; - } - this.state = State.IDLE; - const levelDetails = this.levels[data.frag.level].details; - if (levelDetails) { - this.loadFragment(data.frag, levelDetails, data.frag.start); - } - } - protected onLevelSwitching( event: Events.LEVEL_SWITCHING, data: LevelSwitchingData @@ -291,11 +280,13 @@ export default class BaseStreamController protected onHandlerDestroyed() { this.state = State.STOPPED; - this.hls.off(Events.KEY_LOADED, this.onKeyLoaded, this); this.hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); if (this.fragmentLoader) { this.fragmentLoader.destroy(); } + if (this.keyLoader) { + this.keyLoader.destroy(); + } if (this.decrypter) { this.decrypter.destroy(); } @@ -304,23 +295,13 @@ export default class BaseStreamController this.log = this.warn = this.decrypter = + this.keyLoader = this.fragmentLoader = this.fragmentTracker = null as any; super.onHandlerDestroyed(); } - protected loadKey(frag: Fragment, details: LevelDetails) { - this.log( - `Loading key for ${frag.sn} of [${details.startSN}-${details.endSN}], ${ - this.logPrefix === '[stream-controller]' ? 'level' : 'track' - } ${frag.level}` - ); - this.state = State.KEY_LOADING; - this.fragCurrent = frag; - this.hls.trigger(Events.KEY_LOADING, { frag }); - } - protected loadFragment( frag: Fragment, levelDetails: LevelDetails, @@ -402,8 +383,8 @@ export default class BaseStreamController this.hls.trigger(Events.BUFFER_FLUSHING, flushScope); } - protected _loadInitSegment(frag: Fragment) { - this._doFragLoad(frag) + protected _loadInitSegment(frag: Fragment, details: LevelDetails) { + this._doFragLoad(frag, details) .then((data) => { if (!data || this.fragContextChanged(frag) || !this.levels) { throw new Error('init load aborted'); @@ -554,17 +535,39 @@ export default class BaseStreamController } // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected _handleFragmentLoadProgress(frag: FragLoadedData) {} + protected _handleFragmentLoadProgress( + frag: PartsLoadedData | FragLoadedData + ) {} protected _doFragLoad( frag: Fragment, - details?: LevelDetails, + details: LevelDetails, targetBufferTime: number | null = null, progressCallback?: FragmentLoadProgressCallback ): Promise { if (!this.levels) { throw new Error('frag load aborted, missing levels'); } + + let keyLoadingPromise: Promise | null = null; + if (frag.encrypted && !frag.decryptdata?.key) { + this.log( + `Loading key for ${frag.sn} of [${details.startSN}-${details.endSN}], ${ + this.logPrefix === '[stream-controller]' ? 'level' : 'track' + } ${frag.level}` + ); + this.state = State.KEY_LOADING; + this.fragCurrent = frag; + keyLoadingPromise = this.keyLoader.load(frag).then((keyLoadedData) => { + if (keyLoadedData && !this.fragContextChanged(keyLoadedData.frag)) { + this.hls.trigger(Events.KEY_LOADED, keyLoadedData); + return keyLoadedData; + } + }); + this.hls.trigger(Events.KEY_LOADING, { frag }); + this.throwIfFragContextChanged('KEY_LOADING'); + } + targetBufferTime = Math.max(frag.start, targetBufferTime || 0); if (this.config.lowLatencyMode && details) { const partList = details.partList; @@ -593,6 +596,26 @@ export default class BaseStreamController part: partList[partIndex], targetBufferTime, }); + this.throwIfFragContextChanged('FRAG_LOADING parts'); + if (keyLoadingPromise) { + return keyLoadingPromise + .then((keyLoadedData) => { + if ( + !keyLoadedData || + this.fragContextChanged(keyLoadedData?.frag) + ) { + return null; + } + return this.doFragPartsLoad( + frag, + partList, + partIndex, + progressCallback + ); + }) + .catch((error) => this.handleFragLoadError(error)); + } + return this.doFragPartsLoad( frag, partList, @@ -622,10 +645,44 @@ export default class BaseStreamController } this.state = State.FRAG_LOADING; this.hls.trigger(Events.FRAG_LOADING, { frag, targetBufferTime }); + this.throwIfFragContextChanged('FRAG_LOADING'); + + // Load key before streaming fragment data + const dataOnProgress = this.config.progressive; + if (dataOnProgress && keyLoadingPromise) { + return keyLoadingPromise + .then((keyLoadedData) => { + if (!keyLoadedData || this.fragContextChanged(keyLoadedData?.frag)) { + return null; + } + return this.fragmentLoader.load(frag, progressCallback); + }) + .catch((error) => this.handleFragLoadError(error)); + } - return this.fragmentLoader - .load(frag, progressCallback) - .catch((error: LoadError) => this.handleFragLoadError(error)); + // load unencrypted fragment data with progress event, + // or handle fragment result after key and fragment are finished loading + return Promise.all([ + this.fragmentLoader.load( + frag, + dataOnProgress ? progressCallback : undefined + ), + keyLoadingPromise, + ]) + .then(([fragLoadedData]) => { + if (!dataOnProgress && fragLoadedData && progressCallback) { + progressCallback(fragLoadedData); + } + return fragLoadedData; + }) + .catch((error) => this.handleFragLoadError(error)); + } + + private throwIfFragContextChanged(context: string): void | never { + // exit if context changed during event loop + if (this.fragCurrent === null) { + throw new Error(`frag load aborted, context changed in ${context}`); + } } private doFragPartsLoad( diff --git a/src/controller/stream-controller.ts b/src/controller/stream-controller.ts index 8ba9e55e7bc..c73a183c8ea 100644 --- a/src/controller/stream-controller.ts +++ b/src/controller/stream-controller.ts @@ -320,13 +320,7 @@ export default class StreamController frag = frag.initSegment; } - // We want to load the key if we're dealing with an identity key, because we will decrypt - // this content using the key we fetch. Other keys will be handled by the DRM CDM via EME. - if (frag.decryptdata?.keyFormat === 'identity' && !frag.decryptdata?.key) { - this.loadKey(frag, levelDetails); - } else { - this.loadFragment(frag, levelDetails, targetBufferTime); - } + this.loadFragment(frag, levelDetails, targetBufferTime); } protected loadFragment( @@ -339,12 +333,12 @@ export default class StreamController this.fragCurrent = frag; if (fragState === FragmentState.NOT_LOADED) { if (frag.sn === 'initSegment') { - this._loadInitSegment(frag); + this._loadInitSegment(frag, levelDetails); } else if (this.bitrateTest) { this.log( `Fragment ${frag.sn} of level ${frag.level} is being downloaded to test bitrate and will not be buffered` ); - this._loadBitrateTestFrag(frag); + this._loadBitrateTestFrag(frag, levelDetails); } else { this.startFragRequested = true; super.loadFragment(frag, levelDetails, targetBufferTime); @@ -465,8 +459,8 @@ export default class StreamController const fragCurrent = this.fragCurrent; this.fragCurrent = null; this.backtrackFragment = null; - if (fragCurrent?.loader) { - fragCurrent.loader.abort(); + if (fragCurrent) { + fragCurrent.abortRequests(); } switch (this.state) { case State.KEY_LOADING: @@ -618,7 +612,7 @@ export default class StreamController if (fragCurrent.level !== data.level && fragCurrent.loader) { this.state = State.IDLE; this.backtrackFragment = null; - fragCurrent.loader.abort(); + fragCurrent.abortRequests(); } } @@ -740,9 +734,9 @@ export default class StreamController this.mediaBuffer = this.media; const fragCurrent = this.fragCurrent; // we need to refill audio buffer from main: cancel any frag loading to speed up audio switch - if (fragCurrent?.loader) { + if (fragCurrent) { this.log('Switching to main audio track, cancel main fragment load'); - fragCurrent.loader.abort(); + fragCurrent.abortRequests(); } // destroy transmuxer to force init segment generation (following audio switch) this.resetTransmuxer(); @@ -1013,9 +1007,9 @@ export default class StreamController return audioCodec; } - private _loadBitrateTestFrag(frag: Fragment) { + private _loadBitrateTestFrag(frag: Fragment, levelDetails: LevelDetails) { frag.bitrateTest = true; - this._doFragLoad(frag).then((data) => { + this._doFragLoad(frag, levelDetails).then((data) => { const { hls } = this; if (!data || hls.nextLoadLevel || this.fragContextChanged(frag)) { return; diff --git a/src/controller/subtitle-stream-controller.ts b/src/controller/subtitle-stream-controller.ts index 38d038354b7..2bd46ca3c0f 100644 --- a/src/controller/subtitle-stream-controller.ts +++ b/src/controller/subtitle-stream-controller.ts @@ -182,8 +182,8 @@ export class SubtitleStreamController return; } - if (this.fragCurrent?.loader) { - this.fragCurrent.loader.abort(); + if (this.fragCurrent) { + this.fragCurrent.abortRequests(); } this.state = State.IDLE; @@ -397,16 +397,10 @@ export class SubtitleStreamController return; } - // only load if fragment is not loaded if ( - this.fragmentTracker.getState(foundFrag) !== FragmentState.NOT_LOADED + this.fragmentTracker.getState(foundFrag) === FragmentState.NOT_LOADED ) { - return; - } - - if (foundFrag.encrypted) { - this.loadKey(foundFrag, trackDetails); - } else { + // only load if fragment is not loaded this.loadFragment(foundFrag, trackDetails, targetBufferTime); } } @@ -419,7 +413,7 @@ export class SubtitleStreamController ) { this.fragCurrent = frag; if (frag.sn === 'initSegment') { - this._loadInitSegment(frag); + this._loadInitSegment(frag, levelDetails); } else { super.loadFragment(frag, levelDetails, targetBufferTime); } diff --git a/src/demux/mp4demuxer.ts b/src/demux/mp4demuxer.ts index 0b10a04e51d..868a98b6501 100644 --- a/src/demux/mp4demuxer.ts +++ b/src/demux/mp4demuxer.ts @@ -42,12 +42,11 @@ class MP4Demuxer implements Demuxer { public resetTimeStamp() {} public resetInitSegment( - initSegment: Uint8Array, + initSegment: Uint8Array | undefined, audioCodec: string | undefined, videoCodec: string | undefined, trackDuration: number ) { - const initData = parseInitSegment(initSegment); const videoTrack = (this.videoTrack = dummyTrack( 'video', 1 @@ -64,6 +63,11 @@ class MP4Demuxer implements Demuxer { this.id3Track = dummyTrack('id3', 1) as DemuxedMetadataTrack; this.timeOffset = 0; + if (!initSegment || !initSegment.byteLength) { + return; + } + const initData = parseInitSegment(initSegment); + if (initData.video) { const { id, timescale, codec } = initData.video; videoTrack.id = id; diff --git a/src/hls.ts b/src/hls.ts index cf5133d287a..70a20578cf0 100644 --- a/src/hls.ts +++ b/src/hls.ts @@ -1,6 +1,5 @@ import * as URLToolkit from 'url-toolkit'; import PlaylistLoader from './loader/playlist-loader'; -import KeyLoader from './loader/key-loader'; import ID3TrackController from './controller/id3-track-controller'; import LatencyController from './controller/latency-controller'; import LevelController from './controller/level-controller'; @@ -123,7 +122,6 @@ export default class Hls implements HlsEventEmitter { const fpsController = new ConfigFpsController(this); const playListLoader = new PlaylistLoader(this); - const keyLoader = new KeyLoader(this); const id3TrackController = new ID3TrackController(this); // network controllers @@ -142,7 +140,6 @@ export default class Hls implements HlsEventEmitter { const networkControllers = [ playListLoader, - keyLoader, levelController, streamController, ]; @@ -891,6 +888,7 @@ export type { PlaylistContextType, PlaylistLoaderContext, FragmentLoaderContext, + KeyLoaderContext, Loader, LoaderStats, LoaderContext, diff --git a/src/loader/fragment-loader.ts b/src/loader/fragment-loader.ts index 65aa52b6db9..2d9a476582f 100644 --- a/src/loader/fragment-loader.ts +++ b/src/loader/fragment-loader.ts @@ -7,7 +7,7 @@ import { } from '../types/loader'; import type { HlsConfig } from '../config'; import type { BaseSegment, Part } from './fragment'; -import type { FragLoadedData } from '../types/events'; +import type { FragLoadedData, PartsLoadedData } from '../types/events'; const MIN_CHUNK_SIZE = Math.pow(2, 17); // 128kb @@ -315,4 +315,6 @@ export interface FragLoadFailResult { networkDetails: any; } -export type FragmentLoadProgressCallback = (result: FragLoadedData) => void; +export type FragmentLoadProgressCallback = ( + result: FragLoadedData | PartsLoadedData +) => void; diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts index 18bfe23e351..71674b58bf1 100644 --- a/src/loader/fragment.ts +++ b/src/loader/fragment.ts @@ -4,6 +4,7 @@ import { LevelKey } from './level-key'; import { LoadStats } from './load-stats'; import { AttrList } from '../utils/attr-list'; import type { + KeyLoaderContext, FragmentLoaderContext, Loader, PlaylistLevelType, @@ -109,6 +110,8 @@ export class Fragment extends BaseSegment { public readonly type: PlaylistLevelType; // A reference to the loader. Set while the fragment is loading, and removed afterwards. Used to abort fragment loading public loader: Loader | null = null; + // A reference to the key loader. Set while the key is loading, and removed afterwards. Used to abort key loading + public keyLoader: Loader | null = null; // The level/track index to which the fragment belongs public level: number = -1; // The continuity counter of the fragment @@ -212,6 +215,11 @@ export class Fragment extends BaseSegment { return false; } + abortRequests(): void { + this.loader?.abort(); + this.keyLoader?.abort(); + } + /** * Utility method for parseLevelPlaylist to create an initialization vector for a given segment * @param {number} segmentNumber - segment number to generate IV with diff --git a/src/loader/key-loader.ts b/src/loader/key-loader.ts index e5d808f2e32..64273a344a4 100644 --- a/src/loader/key-loader.ts +++ b/src/loader/key-loader.ts @@ -1,178 +1,184 @@ /* * Decrypt key Loader */ -import { Events } from '../events'; import { ErrorTypes, ErrorDetails } from '../errors'; import { logger } from '../utils/logger'; -import type Hls from '../hls'; -import { Fragment } from './fragment'; import { LoaderStats, LoaderResponse, - LoaderContext, LoaderConfiguration, LoaderCallbacks, Loader, - FragmentLoaderContext, + KeyLoaderContext, } from '../types/loader'; -import type { NetworkComponentAPI } from '../types/component-api'; -import type { KeyLoadingData } from '../types/events'; - -interface KeyLoaderContext extends LoaderContext { - frag: Fragment; -} - -export default class KeyLoader implements NetworkComponentAPI { - private hls: Hls; - public loaders = {}; +import { LoadError } from './fragment-loader'; +import type { HlsConfig } from '../hls'; +import type { Fragment } from '../loader/fragment'; +import type { ComponentAPI } from '../types/component-api'; +import type { KeyLoadedData } from '../types/events'; + +export default class KeyLoader implements ComponentAPI { + private readonly config: HlsConfig; + public loader: Loader | null = null; public decryptkey: Uint8Array | null = null; public decrypturl: string | null = null; - constructor(hls: Hls) { - this.hls = hls; - - this.registerListeners(); - } - - public startLoad(startPosition: number): void {} - - public stopLoad(): void { - this.destroyInternalLoaders(); + constructor(config: HlsConfig) { + this.config = config; } - private registerListeners() { - this.hls.on(Events.KEY_LOADING, this.onKeyLoading, this); - } - - private unregisterListeners() { - this.hls.off(Events.KEY_LOADING, this.onKeyLoading); - } - - private destroyInternalLoaders(): void { - for (const loaderName in this.loaders) { - const loader = this.loaders[loaderName]; - if (loader) { - loader.destroy(); - } - } - this.loaders = {}; + abort(): void { + this.loader?.abort(); } destroy(): void { - this.unregisterListeners(); - this.destroyInternalLoaders(); + if (this.loader) { + this.loader.destroy(); + this.loader = null; + } } - onKeyLoading(event: Events.KEY_LOADING, data: KeyLoadingData) { - const { frag } = data; + load(frag: Fragment): Promise | never { const type = frag.type; - const loader = this.loaders[type]; + const loader = this.loader; if (!frag.decryptdata) { - logger.warn('Missing decryption data on fragment in onKeyLoading'); - return; + throw new Error('Missing decryption data on fragment in onKeyLoading'); } // Load the key if the uri is different from previous one, or if the decrypt key has not yet been retrieved const uri = frag.decryptdata.uri; if (uri !== this.decrypturl || this.decryptkey === null) { - const config = this.hls.config; + const config = this.config; if (loader) { logger.warn(`abort previous key loader for type:${type}`); loader.abort(); } if (!uri) { - logger.warn('key uri is falsy'); - return; + throw new Error('key uri is falsy'); } const Loader = config.loader; - const fragLoader = - (frag.loader = - this.loaders[type] = - new Loader(config) as Loader); + const keyLoader = + (frag.keyLoader = + this.loader = + new Loader(config) as Loader); this.decrypturl = uri; this.decryptkey = null; - const loaderContext: KeyLoaderContext = { - url: uri, - frag: frag, - responseType: 'arraybuffer', - }; - - // maxRetry is 0 so that instead of retrying the same key on the same variant multiple times, - // key-loader will trigger an error and rely on stream-controller to handle retry logic. - // this will also align retry logic with fragment-loader - const loaderConfig: LoaderConfiguration = { - timeout: config.fragLoadingTimeOut, - maxRetry: 0, - retryDelay: config.fragLoadingRetryDelay, - maxRetryDelay: config.fragLoadingMaxRetryTimeout, - highWaterMark: 0, - }; - - const loaderCallbacks: LoaderCallbacks = { - onSuccess: this.loadsuccess.bind(this), - onError: this.loaderror.bind(this), - onTimeout: this.loadtimeout.bind(this), - }; - - fragLoader.load(loaderContext, loaderConfig, loaderCallbacks); + return new Promise((resolve, reject) => { + const loaderContext: KeyLoaderContext = { + url: uri, + frag: frag, + part: null, + responseType: 'arraybuffer', + }; + + // maxRetry is 0 so that instead of retrying the same key on the same variant multiple times, + // key-loader will trigger an error and rely on stream-controller to handle retry logic. + // this will also align retry logic with fragment-loader + const loaderConfig: LoaderConfiguration = { + timeout: config.fragLoadingTimeOut, + maxRetry: 0, + retryDelay: config.fragLoadingRetryDelay, + maxRetryDelay: config.fragLoadingMaxRetryTimeout, + highWaterMark: 0, + }; + + const loaderCallbacks: LoaderCallbacks = { + onSuccess: ( + response: LoaderResponse, + stats: LoaderStats, + context: KeyLoaderContext, + networkDetails: any + ) => { + const frag = context.frag; + if (!frag.decryptdata) { + logger.error('after key load, decryptdata unset'); + return reject( + new LoadError({ + type: ErrorTypes.NETWORK_ERROR, + details: ErrorDetails.KEY_LOAD_ERROR, + fatal: false, + frag, + networkDetails, + }) + ); + } + this.decryptkey = frag.decryptdata.key = new Uint8Array( + response.data as ArrayBuffer + ); + + // detach fragment key loader on load success + frag.keyLoader = null; + this.loader = null; + resolve({ frag }); + }, + + onError: ( + error: { code: number; text: string }, + context: KeyLoaderContext, + networkDetails: any + ) => { + this.resetLoader(context.frag, keyLoader); + reject( + new LoadError({ + type: ErrorTypes.NETWORK_ERROR, + details: ErrorDetails.KEY_LOAD_ERROR, + fatal: false, + frag, + networkDetails, + }) + ); + }, + + onTimeout: ( + stats: LoaderStats, + context: KeyLoaderContext, + networkDetails: any + ) => { + this.resetLoader(context.frag, keyLoader); + reject( + new LoadError({ + type: ErrorTypes.NETWORK_ERROR, + details: ErrorDetails.KEY_LOAD_TIMEOUT, + fatal: false, + frag, + networkDetails, + }) + ); + }, + + onAbort: ( + stats: LoaderStats, + context: KeyLoaderContext, + networkDetails: any + ) => { + this.resetLoader(context.frag, keyLoader); + reject( + new LoadError({ + type: ErrorTypes.NETWORK_ERROR, + details: ErrorDetails.INTERNAL_ABORTED, + fatal: false, + frag, + networkDetails, + }) + ); + }, + }; + + keyLoader.load(loaderContext, loaderConfig, loaderCallbacks); + }); } else if (this.decryptkey) { // Return the key if it's already been loaded frag.decryptdata.key = this.decryptkey; - this.hls.trigger(Events.KEY_LOADED, { frag: frag }); - } - } - - loadsuccess( - response: LoaderResponse, - stats: LoaderStats, - context: KeyLoaderContext - ) { - const frag = context.frag; - if (!frag.decryptdata) { - logger.error('after key load, decryptdata unset'); - return; + return Promise.resolve({ frag }); } - this.decryptkey = frag.decryptdata.key = new Uint8Array( - response.data as ArrayBuffer - ); - - // detach fragment loader on load success - frag.loader = null; - delete this.loaders[frag.type]; - this.hls.trigger(Events.KEY_LOADED, { frag: frag }); + return Promise.resolve(); } - loaderror(response: LoaderResponse, context: KeyLoaderContext) { - const frag = context.frag; - const loader = frag.loader; - if (loader) { - loader.abort(); + private resetLoader(frag: Fragment, loader: Loader) { + if (this.loader === loader) { + this.loader = null; } - - delete this.loaders[frag.type]; - this.hls.trigger(Events.ERROR, { - type: ErrorTypes.NETWORK_ERROR, - details: ErrorDetails.KEY_LOAD_ERROR, - fatal: false, - frag, - response, - }); - } - - loadtimeout(stats: LoaderStats, context: KeyLoaderContext) { - const frag = context.frag; - const loader = frag.loader; - if (loader) { - loader.abort(); - } - - delete this.loaders[frag.type]; - this.hls.trigger(Events.ERROR, { - type: ErrorTypes.NETWORK_ERROR, - details: ErrorDetails.KEY_LOAD_TIMEOUT, - fatal: false, - frag, - }); + loader.destroy(); } } diff --git a/src/types/loader.ts b/src/types/loader.ts index 31ad2c9d016..f13ef56374a 100644 --- a/src/types/loader.ts +++ b/src/types/loader.ts @@ -23,6 +23,8 @@ export interface FragmentLoaderContext extends LoaderContext { part: Part | null; } +export interface KeyLoaderContext extends FragmentLoaderContext {} + export interface LoaderConfiguration { // Max number of load retries maxRetry: number;