From 2074188f78bf7ba5069a405c707b84962a7b9e1f Mon Sep 17 00:00:00 2001 From: grabofus Date: Fri, 25 Apr 2025 13:16:07 +0100 Subject: [PATCH 01/12] fix: pre-set media keys on clear content for key systems provided in eme config --- api-extractor/report/hls.js.api.md | 2 ++ src/controller/base-stream-controller.ts | 36 ++++++++++++++++++++++-- src/controller/eme-controller.ts | 19 +++++++++++++ src/loader/key-loader.ts | 14 ++++++++- 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 7c588abd838..21a9f126db4 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -1140,6 +1140,8 @@ export class EMEController extends Logger implements ComponentAPI { loadKey(data: KeyLoadedData): Promise; // (undocumented) selectKeySystemFormat(frag: Fragment): Promise; + // (undocumented) + test_selectKeySystemFromConfig(keySystemsToAttempt: KeySystems[]): Promise; } // Warning: (ae-missing-release-tag) "EMEControllerConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index e12c676564b..8474098ca52 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -31,6 +31,7 @@ import { getPartWith, updateFragPTSDTS, } from '../utils/level-helper'; +import { getKeySystemsForConfig } from '../utils/mediakeys-helper'; import { appendUint8Array } from '../utils/mp4-tools'; import TimeRanges from '../utils/time-ranges'; import type { FragmentTracker } from './fragment-tracker'; @@ -824,6 +825,9 @@ export default class BaseStreamController this.fragCurrent = frag; keyLoadingPromise = this.keyLoader.load(frag).then((keyLoadedData) => { if (!this.fragContextChanged(keyLoadedData.frag)) { + this.log( + 'Emitting KEY_LOADED event without key info - blame this if something goes wrong', + ); this.hls.trigger(Events.KEY_LOADED, keyLoadedData); if (this.state === State.KEY_LOADING) { this.state = State.IDLE; @@ -837,9 +841,37 @@ export default class BaseStreamController new Error(`frag load aborted, context changed in KEY_LOADING`), ); } - } else if (!frag.encrypted && details.encryptedFragments.length) { - this.keyLoader.loadClear(frag, details.encryptedFragments); + } else if (!frag.encrypted) { + const keySystemsInConfig = getKeySystemsForConfig(this.config); + if (keySystemsInConfig.length) { + this.log( + `Loading keys from config ${JSON.stringify(keySystemsInConfig)}`, + ); + this.state = State.KEY_LOADING; + this.fragCurrent = frag; + keyLoadingPromise = this.keyLoader + .test_loadKeysBeforeFragLoad(frag, keySystemsInConfig) + .then((keyLoadedData) => { + if (!this.fragContextChanged(keyLoadedData.frag)) { + this.hls.trigger(Events.KEY_LOADED, keyLoadedData); + if (this.state === State.KEY_LOADING) { + this.state = State.IDLE; + } + return keyLoadedData; + } + }); + this.hls.trigger(Events.KEY_LOADING, { frag }); + if (this.fragCurrent === null) { + keyLoadingPromise = Promise.reject( + new Error(`frag load aborted, context changed in KEY_LOADING`), + ); + } + } else if (details.encryptedFragments.length) { + this.keyLoader.loadClear(frag, details.encryptedFragments); + } } + // else if (!frag.encrypted && details.encryptedFragments.length) { + // } const fragPrevious = this.fragPrevious; if ( diff --git a/src/controller/eme-controller.ts b/src/controller/eme-controller.ts index 7a4e3b5d77b..65c1b29fc0b 100644 --- a/src/controller/eme-controller.ts +++ b/src/controller/eme-controller.ts @@ -396,6 +396,25 @@ class EMEController extends Logger implements ComponentAPI { return this.keyFormatPromise; } + public test_selectKeySystemFromConfig( + keySystemsToAttempt: KeySystems[], + ): Promise { + return new Promise((resolve, reject) => { + return this.getKeySystemSelectionPromise(keySystemsToAttempt) + .then(({ keySystem }) => { + const keySystemFormat = keySystemToKeySystemFormat(keySystem); + if (keySystemFormat) { + resolve(keySystemFormat); + } else { + reject( + new Error(`Unable to find format for key-system "${keySystem}"`), + ); + } + }) + .catch(reject); + }); + } + private getKeyFormatPromise( keyFormats: KeySystemFormats[], ): Promise { diff --git a/src/loader/key-loader.ts b/src/loader/key-loader.ts index 5042a944d52..75a6b9124c6 100644 --- a/src/loader/key-loader.ts +++ b/src/loader/key-loader.ts @@ -16,7 +16,7 @@ import type { LoaderStats, PlaylistLevelType, } from '../types/loader'; -import type { KeySystemFormats } from '../utils/mediakeys-helper'; +import type { KeySystemFormats, KeySystems } from '../utils/mediakeys-helper'; export interface KeyLoaderInfo { decryptdata: LevelKey; @@ -129,6 +129,18 @@ export default class KeyLoader implements ComponentAPI { return this.loadInternal(frag); } + test_loadKeysBeforeFragLoad( + frag: Fragment, + keySystems: KeySystems[], + ): Promise { + if (this.emeController) { + return this.emeController + ?.test_selectKeySystemFromConfig(keySystems) + .then(() => ({ frag, keyInfo: null as any })); + } + return Promise.resolve({ frag, keyInfo: null as any }); + } + loadInternal( frag: Fragment, keySystemFormat?: KeySystemFormats, From fbd9cac141c92a7db36ffd899a71e2500c80c31b Mon Sep 17 00:00:00 2001 From: grabofus Date: Fri, 25 Apr 2025 13:39:03 +0100 Subject: [PATCH 02/12] add comment --- src/controller/base-stream-controller.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 8474098ca52..35bcce95d28 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -867,11 +867,10 @@ export default class BaseStreamController ); } } else if (details.encryptedFragments.length) { + // TODO: this should also move the state to KEY_LOADING, as we can't buffer unencrypted segments before media keys are set this.keyLoader.loadClear(frag, details.encryptedFragments); } } - // else if (!frag.encrypted && details.encryptedFragments.length) { - // } const fragPrevious = this.fragPrevious; if ( From b76ad064db1a423e278cce9caac8de8f7e3443b6 Mon Sep 17 00:00:00 2001 From: grabofus Date: Fri, 25 Apr 2025 18:22:14 +0100 Subject: [PATCH 03/12] fix: race condition when starting with clean content, refactored key ack logic to key loader --- api-extractor/report/hls.js.api.md | 5 ++- src/config.ts | 1 + src/controller/base-stream-controller.ts | 43 ++++++++---------- src/controller/eme-controller.ts | 56 ++++++++++-------------- src/loader/key-loader.ts | 51 +++++++++++---------- 5 files changed, 70 insertions(+), 86 deletions(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 21a9f126db4..fdf03c095e1 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -1139,9 +1139,9 @@ export class EMEController extends Logger implements ComponentAPI { // (undocumented) loadKey(data: KeyLoadedData): Promise; // (undocumented) - selectKeySystemFormat(frag: Fragment): Promise; + selectKeySystem(keySystemsToAttempt: KeySystems[]): Promise; // (undocumented) - test_selectKeySystemFromConfig(keySystemsToAttempt: KeySystems[]): Promise; + selectKeySystemFormat(frag: Fragment): Promise; } // Warning: (ae-missing-release-tag) "EMEControllerConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1155,6 +1155,7 @@ export type EMEControllerConfig = { drmSystems: DRMSystemsConfiguration; drmSystemOptions: DRMSystemOptions; requestMediaKeySystemAccessFunc: MediaKeyFunc | null; + experimentalKeySystemAccessForClearContent?: boolean; }; // Warning: (ae-missing-release-tag) "ErrorActionFlags" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/config.ts b/src/config.ts index 65ee3f57c97..2d30168be65 100644 --- a/src/config.ts +++ b/src/config.ts @@ -122,6 +122,7 @@ export type EMEControllerConfig = { drmSystems: DRMSystemsConfiguration; drmSystemOptions: DRMSystemOptions; requestMediaKeySystemAccessFunc: MediaKeyFunc | null; + experimentalKeySystemAccessForClearContent?: boolean; }; export interface FragmentLoaderConstructor { diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 35bcce95d28..8c9a3551355 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -842,33 +842,28 @@ export default class BaseStreamController ); } } else if (!frag.encrypted) { - const keySystemsInConfig = getKeySystemsForConfig(this.config); - if (keySystemsInConfig.length) { - this.log( - `Loading keys from config ${JSON.stringify(keySystemsInConfig)}`, - ); + this.log( + `Loading clear ${frag.sn} of [${details.startSN}-${details.endSN}] ${details.encryptedFragments.length ? 'with' : 'without'} encrypted segments in the manifest`, + ); + const keyLoadPromise = this.keyLoader.loadClear( + frag, + details.encryptedFragments, + ); + if (keyLoadPromise) { this.state = State.KEY_LOADING; this.fragCurrent = frag; - keyLoadingPromise = this.keyLoader - .test_loadKeysBeforeFragLoad(frag, keySystemsInConfig) - .then((keyLoadedData) => { - if (!this.fragContextChanged(keyLoadedData.frag)) { - this.hls.trigger(Events.KEY_LOADED, keyLoadedData); - if (this.state === State.KEY_LOADING) { - this.state = State.IDLE; - } - return keyLoadedData; + // Note: Omitted KEY_LOADING event here as we didn't have that for loadClear + keyLoadingPromise = keyLoadPromise.then(() => { + // TODO: Not sure about this. Do we need to check if the current fragment changed? + // For clear segments even if we get the first encrypted fragment back, that won't + // match the current fragment of the stream controller. + if (!this.fragContextChanged(frag)) { + // Note: Omitted KEY_LOADED event here as we didn't have that for loadClear + if (this.state === State.KEY_LOADING) { + this.state = State.IDLE; } - }); - this.hls.trigger(Events.KEY_LOADING, { frag }); - if (this.fragCurrent === null) { - keyLoadingPromise = Promise.reject( - new Error(`frag load aborted, context changed in KEY_LOADING`), - ); - } - } else if (details.encryptedFragments.length) { - // TODO: this should also move the state to KEY_LOADING, as we can't buffer unencrypted segments before media keys are set - this.keyLoader.loadClear(frag, details.encryptedFragments); + } + }); } } diff --git a/src/controller/eme-controller.ts b/src/controller/eme-controller.ts index 65c1b29fc0b..b3d56209172 100644 --- a/src/controller/eme-controller.ts +++ b/src/controller/eme-controller.ts @@ -383,20 +383,7 @@ class EMEController extends Logger implements ComponentAPI { return keySession.update(data); } - public selectKeySystemFormat(frag: Fragment): Promise { - const keyFormats = Object.keys(frag.levelkeys || {}) as KeySystemFormats[]; - if (!this.keyFormatPromise) { - this.log( - `Selecting key-system from fragment (sn: ${frag.sn} ${frag.type}: ${ - frag.level - }) key formats ${keyFormats.join(', ')}`, - ); - this.keyFormatPromise = this.getKeyFormatPromise(keyFormats); - } - return this.keyFormatPromise; - } - - public test_selectKeySystemFromConfig( + public selectKeySystem( keySystemsToAttempt: KeySystems[], ): Promise { return new Promise((resolve, reject) => { @@ -415,29 +402,30 @@ class EMEController extends Logger implements ComponentAPI { }); } + public selectKeySystemFormat(frag: Fragment): Promise { + const keyFormats = Object.keys(frag.levelkeys || {}) as KeySystemFormats[]; + if (!this.keyFormatPromise) { + this.log( + `Selecting key-system from fragment (sn: ${frag.sn} ${frag.type}: ${ + frag.level + }) key formats ${keyFormats.join(', ')}`, + ); + this.keyFormatPromise = this.getKeyFormatPromise(keyFormats); + } + return this.keyFormatPromise; + } + private getKeyFormatPromise( keyFormats: KeySystemFormats[], ): Promise { - return new Promise((resolve, reject) => { - const keySystemsInConfig = getKeySystemsForConfig(this.config); - const keySystemsToAttempt = keyFormats - .map(keySystemFormatToKeySystemDomain) - .filter( - (value) => !!value && keySystemsInConfig.indexOf(value) !== -1, - ) as any as KeySystems[]; - return this.getKeySystemSelectionPromise(keySystemsToAttempt) - .then(({ keySystem }) => { - const keySystemFormat = keySystemToKeySystemFormat(keySystem); - if (keySystemFormat) { - resolve(keySystemFormat); - } else { - reject( - new Error(`Unable to find format for key-system "${keySystem}"`), - ); - } - }) - .catch(reject); - }); + const keySystemsInConfig = getKeySystemsForConfig(this.config); + const keySystemsToAttempt = keyFormats + .map(keySystemFormatToKeySystemDomain) + .filter( + (value) => !!value && keySystemsInConfig.indexOf(value) !== -1, + ) as any as KeySystems[]; + + return this.selectKeySystem(keySystemsToAttempt); } public loadKey(data: KeyLoadedData): Promise { diff --git a/src/loader/key-loader.ts b/src/loader/key-loader.ts index 75a6b9124c6..43d5a4d85f4 100644 --- a/src/loader/key-loader.ts +++ b/src/loader/key-loader.ts @@ -1,5 +1,6 @@ import { LoadError } from './fragment-loader'; import { ErrorDetails, ErrorTypes } from '../errors'; +import { getKeySystemsForConfig } from '../utils/mediakeys-helper'; import type { HlsConfig } from '../config'; import type { LevelKey } from './level-key'; import type EMEController from '../controller/eme-controller'; @@ -16,7 +17,7 @@ import type { LoaderStats, PlaylistLevelType, } from '../types/loader'; -import type { KeySystemFormats, KeySystems } from '../utils/mediakeys-helper'; +import type { KeySystemFormats } from '../utils/mediakeys-helper'; export interface KeyLoaderInfo { decryptdata: LevelKey; @@ -92,20 +93,30 @@ export default class KeyLoader implements ComponentAPI { encryptedFragments: Fragment[], ): void | Promise { if (this.emeController && this.config.emeEnabled) { - // access key-system with nearest key on start (loaidng frag is unencrypted) - const { sn, cc } = loadingFrag; - for (let i = 0; i < encryptedFragments.length; i++) { - const frag = encryptedFragments[i]; - if ( - cc <= frag.cc && - (sn === 'initSegment' || frag.sn === 'initSegment' || sn < frag.sn) - ) { - this.emeController - .selectKeySystemFormat(frag) - .then((keySystemFormat) => { - frag.setKeyFormat(keySystemFormat); + // access key-system with nearest key on start (loading frag is unencrypted) + if (encryptedFragments.length) { + const { sn, cc } = loadingFrag; + for (let i = 0; i < encryptedFragments.length; i++) { + const frag = encryptedFragments[i]; + if ( + cc <= frag.cc && + (sn === 'initSegment' || frag.sn === 'initSegment' || sn < frag.sn) + ) { + return this.emeController + .selectKeySystemFormat(frag) + .then((keySystemFormat) => { + frag.setKeyFormat(keySystemFormat); + }); + } + } + } else if (this.config.experimentalKeySystemAccessForClearContent) { + const keySystemsInConfig = getKeySystemsForConfig(this.config); + if (keySystemsInConfig.length) { + return this.emeController + .selectKeySystem(keySystemsInConfig) + .then(() => { + /* void */ }); - break; } } } @@ -129,18 +140,6 @@ export default class KeyLoader implements ComponentAPI { return this.loadInternal(frag); } - test_loadKeysBeforeFragLoad( - frag: Fragment, - keySystems: KeySystems[], - ): Promise { - if (this.emeController) { - return this.emeController - ?.test_selectKeySystemFromConfig(keySystems) - .then(() => ({ frag, keyInfo: null as any })); - } - return Promise.resolve({ frag, keyInfo: null as any }); - } - loadInternal( frag: Fragment, keySystemFormat?: KeySystemFormats, From 8562cc2131b73da6de4c7e9086e7c792bf689d6d Mon Sep 17 00:00:00 2001 From: grabofus Date: Fri, 25 Apr 2025 18:23:46 +0100 Subject: [PATCH 04/12] removed log --- src/controller/base-stream-controller.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 8c9a3551355..66124542e25 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -825,9 +825,6 @@ export default class BaseStreamController this.fragCurrent = frag; keyLoadingPromise = this.keyLoader.load(frag).then((keyLoadedData) => { if (!this.fragContextChanged(keyLoadedData.frag)) { - this.log( - 'Emitting KEY_LOADED event without key info - blame this if something goes wrong', - ); this.hls.trigger(Events.KEY_LOADED, keyLoadedData); if (this.state === State.KEY_LOADING) { this.state = State.IDLE; From ff807211e18d80eb382cd62d50c4a56f828676ba Mon Sep 17 00:00:00 2001 From: grabofus Date: Tue, 29 Apr 2025 01:18:12 +0100 Subject: [PATCH 05/12] rename experimental config option to requireKeySystemAccessOnStart --- api-extractor/report/hls.js.api.md | 2 +- src/config.ts | 3 ++- src/loader/key-loader.ts | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index fdf03c095e1..2fe9a1ddd1a 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -1155,7 +1155,7 @@ export type EMEControllerConfig = { drmSystems: DRMSystemsConfiguration; drmSystemOptions: DRMSystemOptions; requestMediaKeySystemAccessFunc: MediaKeyFunc | null; - experimentalKeySystemAccessForClearContent?: boolean; + requireKeySystemAccessOnStart?: boolean; }; // Warning: (ae-missing-release-tag) "ErrorActionFlags" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/config.ts b/src/config.ts index 2d30168be65..e6ee30409bd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -122,7 +122,7 @@ export type EMEControllerConfig = { drmSystems: DRMSystemsConfiguration; drmSystemOptions: DRMSystemOptions; requestMediaKeySystemAccessFunc: MediaKeyFunc | null; - experimentalKeySystemAccessForClearContent?: boolean; + requireKeySystemAccessOnStart: boolean; }; export interface FragmentLoaderConstructor { @@ -433,6 +433,7 @@ export const hlsDefaultConfig: HlsConfig = { requestMediaKeySystemAccessFunc: __USE_EME_DRM__ ? requestMediaKeySystemAccess : null, // used by eme-controller + requireKeySystemAccessOnStart: false, // used by eme-controller testBandwidth: true, progressive: false, lowLatencyMode: true, diff --git a/src/loader/key-loader.ts b/src/loader/key-loader.ts index 43d5a4d85f4..c7ab993f469 100644 --- a/src/loader/key-loader.ts +++ b/src/loader/key-loader.ts @@ -91,7 +91,7 @@ export default class KeyLoader implements ComponentAPI { loadClear( loadingFrag: Fragment, encryptedFragments: Fragment[], - ): void | Promise { + ): null | Promise { if (this.emeController && this.config.emeEnabled) { // access key-system with nearest key on start (loading frag is unencrypted) if (encryptedFragments.length) { @@ -109,7 +109,7 @@ export default class KeyLoader implements ComponentAPI { }); } } - } else if (this.config.experimentalKeySystemAccessForClearContent) { + } else if (this.config.requireKeySystemAccessOnStart) { const keySystemsInConfig = getKeySystemsForConfig(this.config); if (keySystemsInConfig.length) { return this.emeController @@ -120,6 +120,7 @@ export default class KeyLoader implements ComponentAPI { } } } + return null; } load(frag: Fragment): Promise { From 4d2a86d8a850031cd504b4f9427d375f1ae01933 Mon Sep 17 00:00:00 2001 From: grabofus Date: Tue, 29 Apr 2025 01:18:38 +0100 Subject: [PATCH 06/12] simplify loadClear flow --- src/controller/base-stream-controller.ts | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 66124542e25..98db989587b 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -839,28 +839,12 @@ export default class BaseStreamController ); } } else if (!frag.encrypted) { - this.log( - `Loading clear ${frag.sn} of [${details.startSN}-${details.endSN}] ${details.encryptedFragments.length ? 'with' : 'without'} encrypted segments in the manifest`, - ); - const keyLoadPromise = this.keyLoader.loadClear( + keyLoadingPromise = this.keyLoader.loadClear( frag, details.encryptedFragments, ); - if (keyLoadPromise) { - this.state = State.KEY_LOADING; - this.fragCurrent = frag; - // Note: Omitted KEY_LOADING event here as we didn't have that for loadClear - keyLoadingPromise = keyLoadPromise.then(() => { - // TODO: Not sure about this. Do we need to check if the current fragment changed? - // For clear segments even if we get the first encrypted fragment back, that won't - // match the current fragment of the stream controller. - if (!this.fragContextChanged(frag)) { - // Note: Omitted KEY_LOADED event here as we didn't have that for loadClear - if (this.state === State.KEY_LOADING) { - this.state = State.IDLE; - } - } - }); + if (keyLoadingPromise) { + this.log(`[eme] blocking frag load until media-keys acquired`); } } From 08fc040e32907e65533d97767d90de3c1a52ad48 Mon Sep 17 00:00:00 2001 From: grabofus Date: Tue, 29 Apr 2025 01:51:39 +0100 Subject: [PATCH 07/12] remove optional from docs --- api-extractor/report/hls.js.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 2fe9a1ddd1a..b0dd3a54541 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -1155,7 +1155,7 @@ export type EMEControllerConfig = { drmSystems: DRMSystemsConfiguration; drmSystemOptions: DRMSystemOptions; requestMediaKeySystemAccessFunc: MediaKeyFunc | null; - requireKeySystemAccessOnStart?: boolean; + requireKeySystemAccessOnStart: boolean; }; // Warning: (ae-missing-release-tag) "ErrorActionFlags" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) From ee93beddd25bbfd7f8d51d012efa5f2b1aae9746 Mon Sep 17 00:00:00 2001 From: grabofus Date: Tue, 29 Apr 2025 16:48:08 +0100 Subject: [PATCH 08/12] added todo --- src/controller/base-stream-controller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 98db989587b..c2d9ff13c29 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -844,6 +844,8 @@ export default class BaseStreamController details.encryptedFragments, ); if (keyLoadingPromise) { + // TODO: This gets logged on every clear segment load regardless of having key system initialized. + // keyLoader.loadClear should be optimized to not return a promise if we don't need to wait for key system access. this.log(`[eme] blocking frag load until media-keys acquired`); } } From 05b5da35a3f19d17b90c411fcd136a1377e9d012 Mon Sep 17 00:00:00 2001 From: grabofus Date: Thu, 1 May 2025 21:04:17 +0100 Subject: [PATCH 09/12] fix: avoid unnecessarily waiting when loading clear segments --- api-extractor/report/hls.js.api.md | 4 +++- src/controller/eme-controller.ts | 13 +++++++++++++ src/loader/key-loader.ts | 5 ++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index b0dd3a54541..2ba830b973e 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -1137,6 +1137,8 @@ export class EMEController extends Logger implements ComponentAPI { // (undocumented) destroy(): void; // (undocumented) + getSelectedKeySystemFormats(): KeySystemFormats[]; + // (undocumented) loadKey(data: KeyLoadedData): Promise; // (undocumented) selectKeySystem(keySystemsToAttempt: KeySystems[]): Promise; @@ -2920,7 +2922,7 @@ export class KeyLoader implements ComponentAPI { // (undocumented) load(frag: Fragment): Promise; // (undocumented) - loadClear(loadingFrag: Fragment, encryptedFragments: Fragment[]): void | Promise; + loadClear(loadingFrag: Fragment, encryptedFragments: Fragment[]): null | Promise; // (undocumented) loadInternal(frag: Fragment, keySystemFormat?: KeySystemFormats): Promise; // (undocumented) diff --git a/src/controller/eme-controller.ts b/src/controller/eme-controller.ts index b3d56209172..ea0f03db4c0 100644 --- a/src/controller/eme-controller.ts +++ b/src/controller/eme-controller.ts @@ -51,6 +51,7 @@ interface KeySystemAccessPromises { keySystemAccess: Promise; mediaKeys?: Promise; certificate?: Promise; + hasMediaKeys?: boolean; } export interface MediaKeySessionContext { @@ -284,6 +285,7 @@ class EMEController extends Logger implements ComponentAPI { .createMediaKeys() .then((mediaKeys) => { this.log(`Media-keys created for "${keySystem}"`); + keySystemAccessPromises.hasMediaKeys = true; return certificateRequest.then((certificate) => { if (certificate) { return this.setMediaKeysServerCertificate( @@ -383,6 +385,17 @@ class EMEController extends Logger implements ComponentAPI { return keySession.update(data); } + public getSelectedKeySystemFormats(): KeySystemFormats[] { + return (Object.keys(this.keySystemAccessPromises) as KeySystems[]) + .map((keySystem) => ({ + keySystem, + hasMediaKeys: this.keySystemAccessPromises[keySystem].hasMediaKeys, + })) + .filter(({ hasMediaKeys }) => !!hasMediaKeys) + .map(({ keySystem }) => keySystemToKeySystemFormat(keySystem)) + .filter((keySystem): keySystem is KeySystemFormats => !!keySystem); + } + public selectKeySystem( keySystemsToAttempt: KeySystems[], ): Promise { diff --git a/src/loader/key-loader.ts b/src/loader/key-loader.ts index c7ab993f469..dbce42b8d3a 100644 --- a/src/loader/key-loader.ts +++ b/src/loader/key-loader.ts @@ -109,7 +109,10 @@ export default class KeyLoader implements ComponentAPI { }); } } - } else if (this.config.requireKeySystemAccessOnStart) { + } else if ( + this.config.requireKeySystemAccessOnStart && + !this.emeController.getSelectedKeySystemFormats().length + ) { const keySystemsInConfig = getKeySystemsForConfig(this.config); if (keySystemsInConfig.length) { return this.emeController From 2d58dcdb15eed587cb1701a0e380f326113fbd1a Mon Sep 17 00:00:00 2001 From: grabofus Date: Thu, 1 May 2025 21:20:57 +0100 Subject: [PATCH 10/12] remove todo --- src/controller/base-stream-controller.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index c2d9ff13c29..98db989587b 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -844,8 +844,6 @@ export default class BaseStreamController details.encryptedFragments, ); if (keyLoadingPromise) { - // TODO: This gets logged on every clear segment load regardless of having key system initialized. - // keyLoader.loadClear should be optimized to not return a promise if we don't need to wait for key system access. this.log(`[eme] blocking frag load until media-keys acquired`); } } From c7c4c6a0963c193c812710cd3bc5503c3c010cb1 Mon Sep 17 00:00:00 2001 From: grabofus Date: Fri, 21 Jun 2024 17:12:03 +0100 Subject: [PATCH 11/12] fix: Transition from Clear to DRM content (originally https://github.com/DiceTechnology/hls.js/commit/82f6aea7885ca6701418d6805adceae37629f7dd) --- src/controller/eme-controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controller/eme-controller.ts b/src/controller/eme-controller.ts index ea0f03db4c0..241e093f1aa 100644 --- a/src/controller/eme-controller.ts +++ b/src/controller/eme-controller.ts @@ -294,6 +294,7 @@ class EMEController extends Logger implements ComponentAPI { certificate, ); } + this.attemptSetMediaKeys(keySystem, mediaKeys); return mediaKeys; }); }); From a3ecbb1068c3665338b38d866c5bc6cee9c26315 Mon Sep 17 00:00:00 2001 From: grabofus <34451173+grabofus@users.noreply.github.com> Date: Fri, 9 May 2025 15:27:47 +0200 Subject: [PATCH 12/12] refactor loadClear --- api-extractor/report/hls.js.api.md | 2 ++ src/controller/eme-controller.ts | 9 +++++++- src/loader/key-loader.ts | 34 +++++++++++++++++++----------- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 2ba830b973e..e0b49016893 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -1137,6 +1137,8 @@ export class EMEController extends Logger implements ComponentAPI { // (undocumented) destroy(): void; // (undocumented) + getKeySystemAccess(keySystemsToAttempt: KeySystems[]): Promise; + // (undocumented) getSelectedKeySystemFormats(): KeySystemFormats[]; // (undocumented) loadKey(data: KeyLoadedData): Promise; diff --git a/src/controller/eme-controller.ts b/src/controller/eme-controller.ts index 241e093f1aa..6ffdfca6f19 100644 --- a/src/controller/eme-controller.ts +++ b/src/controller/eme-controller.ts @@ -294,7 +294,6 @@ class EMEController extends Logger implements ComponentAPI { certificate, ); } - this.attemptSetMediaKeys(keySystem, mediaKeys); return mediaKeys; }); }); @@ -397,6 +396,14 @@ class EMEController extends Logger implements ComponentAPI { .filter((keySystem): keySystem is KeySystemFormats => !!keySystem); } + public getKeySystemAccess(keySystemsToAttempt: KeySystems[]): Promise { + return this.getKeySystemSelectionPromise(keySystemsToAttempt).then( + ({ keySystem, mediaKeys }) => { + return this.attemptSetMediaKeys(keySystem, mediaKeys); + }, + ); + } + public selectKeySystem( keySystemsToAttempt: KeySystems[], ): Promise { diff --git a/src/loader/key-loader.ts b/src/loader/key-loader.ts index dbce42b8d3a..de0b1b1315c 100644 --- a/src/loader/key-loader.ts +++ b/src/loader/key-loader.ts @@ -1,8 +1,11 @@ import { LoadError } from './fragment-loader'; import { ErrorDetails, ErrorTypes } from '../errors'; -import { getKeySystemsForConfig } from '../utils/mediakeys-helper'; -import type { HlsConfig } from '../config'; +import { + getKeySystemsForConfig, + keySystemFormatToKeySystemDomain, +} from '../utils/mediakeys-helper'; import type { LevelKey } from './level-key'; +import type { HlsConfig } from '../config'; import type EMEController from '../controller/eme-controller'; import type { MediaKeySessionContext } from '../controller/eme-controller'; import type { Fragment } from '../loader/fragment'; @@ -92,7 +95,11 @@ export default class KeyLoader implements ComponentAPI { loadingFrag: Fragment, encryptedFragments: Fragment[], ): null | Promise { - if (this.emeController && this.config.emeEnabled) { + if ( + this.emeController && + this.config.emeEnabled && + !this.emeController.getSelectedKeySystemFormats().length + ) { // access key-system with nearest key on start (loading frag is unencrypted) if (encryptedFragments.length) { const { sn, cc } = loadingFrag; @@ -106,20 +113,23 @@ export default class KeyLoader implements ComponentAPI { .selectKeySystemFormat(frag) .then((keySystemFormat) => { frag.setKeyFormat(keySystemFormat); + if ( + this.emeController && + this.config.requireKeySystemAccessOnStart + ) { + const keySystem = + keySystemFormatToKeySystemDomain(keySystemFormat); + if (keySystem) { + return this.emeController.getKeySystemAccess([keySystem]); + } + } }); } } - } else if ( - this.config.requireKeySystemAccessOnStart && - !this.emeController.getSelectedKeySystemFormats().length - ) { + } else if (this.config.requireKeySystemAccessOnStart) { const keySystemsInConfig = getKeySystemsForConfig(this.config); if (keySystemsInConfig.length) { - return this.emeController - .selectKeySystem(keySystemsInConfig) - .then(() => { - /* void */ - }); + return this.emeController.getKeySystemAccess(keySystemsInConfig); } } }