From 4a79f0809a456ad0014cb2e2253107708587bd2c Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Thu, 21 Nov 2024 15:50:25 +0100 Subject: [PATCH 1/3] Set-up a comprehensive check when trying to reuse MediaKeySystemAccess While working on cases where an application wants to set-up different DRM configurations depending on the content, yet still keep the same `MediaKeys` instance if possible (to keep our `MediaKeySession` cache), I tried to make sure we support at worse a case where a more constrained configuration would still be considered as compatible to a less constrained one - as long as there's no loss of features / capabilities between the two. After attempts specific to the RxPlayer's `keySystems` API format, I ended up relying on the asked `MediaKeySystemConfiguration`s (both the new wanted one and the one that led to the current `MediaKeySystemAccess`) instead, as the corresponding logic seemed easier to me to maintain. --- src/main_thread/decrypt/find_key_system.ts | 150 +++++++++++++-------- 1 file changed, 93 insertions(+), 57 deletions(-) diff --git a/src/main_thread/decrypt/find_key_system.ts b/src/main_thread/decrypt/find_key_system.ts index ab49fbc6cb..68e05500a6 100644 --- a/src/main_thread/decrypt/find_key_system.ts +++ b/src/main_thread/decrypt/find_key_system.ts @@ -88,52 +88,71 @@ interface IKeySystemType { } /** - * @param {Object} keySystem - * @param {Object} askedConfiguration + * @param {Object} newConfiguration + * @param {Object} prevConfiguration * @param {MediaKeySystemAccess} currentKeySystemAccess - * @param {Object} currentKeySystemOptions * @returns {null|Object} */ function checkCachedMediaKeySystemAccess( - keySystem: IKeySystemOption, - askedConfiguration: MediaKeySystemConfiguration, + newConfiguration: MediaKeySystemConfiguration, + prevConfiguration: MediaKeySystemConfiguration, currentKeySystemAccess: MediaKeySystemAccess | ICustomMediaKeySystemAccess, - currentKeySystemOptions: IKeySystemOption, -): null | { - keySystemOptions: IKeySystemOption; - askedConfiguration: MediaKeySystemConfiguration; - keySystemAccess: MediaKeySystemAccess | ICustomMediaKeySystemAccess; -} { +): null | MediaKeySystemAccess | ICustomMediaKeySystemAccess { const mksConfiguration = currentKeySystemAccess.getConfiguration(); if (shouldRenewMediaKeySystemAccess() || isNullOrUndefined(mksConfiguration)) { return null; } - // TODO Do it with MediaKeySystemAccess.prototype.keySystem instead - if (keySystem.type !== currentKeySystemOptions.type) { + if (newConfiguration.label !== prevConfiguration.label) { return null; } - if ( - (!isNullOrUndefined(keySystem.persistentLicenseConfig) || - keySystem.persistentState === "required") && - mksConfiguration.persistentState !== "required" - ) { + const prevDistinctiveIdentifier = prevConfiguration.distinctiveIdentifier ?? "optional"; + const newDistinctiveIdentifier = newConfiguration.distinctiveIdentifier ?? "optional"; + if (prevDistinctiveIdentifier !== newDistinctiveIdentifier) { return null; } - if ( - keySystem.distinctiveIdentifier === "required" && - mksConfiguration.distinctiveIdentifier !== "required" - ) { + const prevPersistentState = prevConfiguration.persistentState ?? "optional"; + const newPersistentState = newConfiguration.persistentState ?? "optional"; + if (prevPersistentState !== newPersistentState) { return null; } - return { - keySystemOptions: keySystem, - keySystemAccess: currentKeySystemAccess, - askedConfiguration, - }; + const prevInitDataTypes = prevConfiguration.initDataTypes ?? []; + const newInitDataTypes = newConfiguration.initDataTypes ?? []; + if (!isArraySubsetOf(newInitDataTypes, prevInitDataTypes)) { + return null; + } + + const prevSessionTypes = prevConfiguration.sessionTypes ?? []; + const newSessionTypes = newConfiguration.sessionTypes ?? []; + if (!isArraySubsetOf(newSessionTypes, prevSessionTypes)) { + return null; + } + + for (const prop of ["audioCapabilities", "videoCapabilities"] as const) { + const newCapabilities = newConfiguration[prop] ?? []; + const prevCapabilities = prevConfiguration[prop] ?? []; + const wasFound = newCapabilities.every((n) => { + for (let i = 0; i < prevCapabilities.length; i++) { + const prevCap = prevCapabilities[i]; + if ( + (prevCap.robustness ?? "") === (n.robustness ?? "") || + (prevCap.encryptionScheme ?? null) === (n.encryptionScheme ?? null) || + (prevCap.robustness ?? "") === (n.robustness ?? "") + ) { + return true; + } + } + return false; + }); + if (!wasFound) { + return null; + } + } + + return currentKeySystemAccess; } /** @@ -444,36 +463,6 @@ export default function getMediaKeySystemAccess( } const chosenType = keySystemsType[index]; - - const currentState = await MediaKeysAttacher.getAttachedMediaKeysState(mediaElement); - if (currentState !== null) { - if (eme.implementation === currentState.emeImplementation.implementation) { - // Fast way to find a compatible keySystem if the currently loaded - // one as exactly the same compatibility options. - const cachedKeySystemAccess = checkCachedMediaKeySystemAccess( - chosenType.keySystemOptions, - currentState.askedConfiguration, - currentState.mediaKeySystemAccess, - currentState.keySystemOptions, - ); - if (cachedKeySystemAccess !== null) { - log.info("DRM: Found cached compatible keySystem"); - return Promise.resolve({ - type: "reuse-media-key-system-access" as const, - value: { - mediaKeySystemAccess: cachedKeySystemAccess.keySystemAccess, - askedConfiguration: cachedKeySystemAccess.askedConfiguration, - options: cachedKeySystemAccess.keySystemOptions, - codecSupport: extractCodecSupportListFromConfiguration( - cachedKeySystemAccess.askedConfiguration, - cachedKeySystemAccess.keySystemAccess.getConfiguration(), - ), - }, - }); - } - } - } - const { keyType, keySystemOptions } = chosenType; const keySystemConfigurations = buildKeySystemConfigurations(chosenType); @@ -484,8 +473,40 @@ export default function getMediaKeySystemAccess( ); let keySystemAccess; + const currentState = await MediaKeysAttacher.getAttachedMediaKeysState(mediaElement); for (let configIdx = 0; configIdx < keySystemConfigurations.length; configIdx++) { const keySystemConfiguration = keySystemConfigurations[configIdx]; + if (currentState !== null) { + if ( + // TODO Do it with MediaKeySystemAccess.prototype.keySystem instead + keyType === currentState.keySystemOptions.type && + eme.implementation === currentState.emeImplementation.implementation + ) { + // Fast way to find a compatible keySystem if the currently loaded + // one as exactly the same compatibility options. + const cachedKeySystemAccess = checkCachedMediaKeySystemAccess( + keySystemConfiguration, + currentState.askedConfiguration, + currentState.mediaKeySystemAccess, + ); + if (cachedKeySystemAccess !== null) { + log.info("DRM: Found cached compatible keySystem"); + return Promise.resolve({ + type: "reuse-media-key-system-access" as const, + value: { + mediaKeySystemAccess: cachedKeySystemAccess, + askedConfiguration: currentState.askedConfiguration, + options: currentState.keySystemOptions, + codecSupport: extractCodecSupportListFromConfiguration( + currentState.askedConfiguration, + cachedKeySystemAccess.getConfiguration(), + ), + }, + }); + } + } + } + try { keySystemAccess = await testKeySystem(keyType, [keySystemConfiguration]); log.info("DRM: Found compatible keysystem", keyType, index + 1); @@ -544,3 +565,18 @@ export async function testKeySystem( } return keySystemAccess; } + +/** + * Returns `true` if `arr1`'s values are entirely contained in `arr2`. + * @param {string} arr1 + * @param {string} arr2 + * @return {boolean} + */ +function isArraySubsetOf(arr1: string[], arr2: string[]): boolean { + for (let i = 0; i < arr1.length; i++) { + if (!arrayIncludes(arr2, arr1[i])) { + return false; + } + } + return true; +} From 4410d6d701827057648e07c6defa054060123934 Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Thu, 12 Dec 2024 12:06:35 +0100 Subject: [PATCH 2/3] DRM: simplify some code documentation --- src/main_thread/decrypt/find_key_system.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/main_thread/decrypt/find_key_system.ts b/src/main_thread/decrypt/find_key_system.ts index 68e05500a6..bb98d7f135 100644 --- a/src/main_thread/decrypt/find_key_system.ts +++ b/src/main_thread/decrypt/find_key_system.ts @@ -81,9 +81,9 @@ interface IKeySystemType { * system. */ keyName: string | undefined; - /** keyType: keySystem type (e.g. "com.widevine.alpha") */ + /** KeySystem type (e.g. "com.widevine.alpha") */ keyType: string; - /** keySystem {Object}: the original keySystem object */ + /** The original keySystem object */ keySystemOptions: IKeySystemOption; } @@ -403,14 +403,7 @@ export default function getMediaKeySystemAccess( cancelSignal: CancellationSignal, ): Promise { log.info("DRM: Searching for compatible MediaKeySystemAccess"); - /** - * Array of set keySystems for this content. - * Each item of this array is an object containing the following keys: - * - keyName {string}: keySystem canonical name (e.g. "widevine") - * - keyType {string}: keySystem type (e.g. "com.widevine.alpha") - * - keySystem {Object}: the original keySystem object - * @type {Array.} - */ + /** Array of set keySystems for this content. */ const keySystemsType: IKeySystemType[] = keySystemsConfigs.reduce( (arr: IKeySystemType[], keySystemOptions) => { const { EME_KEY_SYSTEMS } = config.getCurrent(); From 87df748448c9e1ead5f9bbaab04d790dff7e825d Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Thu, 12 Dec 2024 12:24:16 +0100 Subject: [PATCH 3/3] DRM: make MediaKeySystemAccess reusage code more readable --- src/main_thread/decrypt/find_key_system.ts | 96 +++++++++++----------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/src/main_thread/decrypt/find_key_system.ts b/src/main_thread/decrypt/find_key_system.ts index bb98d7f135..6de768fe1c 100644 --- a/src/main_thread/decrypt/find_key_system.ts +++ b/src/main_thread/decrypt/find_key_system.ts @@ -88,47 +88,50 @@ interface IKeySystemType { } /** - * @param {Object} newConfiguration - * @param {Object} prevConfiguration - * @param {MediaKeySystemAccess} currentKeySystemAccess - * @returns {null|Object} + * Takes a `newConfiguration` `MediaKeySystemConfiguration`, that is intended + * for the creation of a `MediaKeySystemAccess`, and a `prevConfiguration` + * `MediaKeySystemConfiguration`, that was the one used at creation of the + * current `MediaKeySystemAccess`. + * + * This function will then return `true` if it determined that the new + * configuration is conceptually compatible with the one used before, and + * `false` otherwise. + * @param {Object} newConfiguration - New wanted `MediaKeySystemConfiguration` + * @param {Object} prevConfiguration - The `MediaKeySystemConfiguration` that is + * relied on util now. + * @returns {boolean} - `true` if `newConfiguration` is compatible with + * `prevConfiguration`. */ -function checkCachedMediaKeySystemAccess( +function isNewMediaKeySystemConfigurationCompatibleWithPreviousOne( newConfiguration: MediaKeySystemConfiguration, prevConfiguration: MediaKeySystemConfiguration, - currentKeySystemAccess: MediaKeySystemAccess | ICustomMediaKeySystemAccess, -): null | MediaKeySystemAccess | ICustomMediaKeySystemAccess { - const mksConfiguration = currentKeySystemAccess.getConfiguration(); - if (shouldRenewMediaKeySystemAccess() || isNullOrUndefined(mksConfiguration)) { - return null; - } - +): boolean { if (newConfiguration.label !== prevConfiguration.label) { - return null; + return false; } const prevDistinctiveIdentifier = prevConfiguration.distinctiveIdentifier ?? "optional"; const newDistinctiveIdentifier = newConfiguration.distinctiveIdentifier ?? "optional"; if (prevDistinctiveIdentifier !== newDistinctiveIdentifier) { - return null; + return false; } const prevPersistentState = prevConfiguration.persistentState ?? "optional"; const newPersistentState = newConfiguration.persistentState ?? "optional"; if (prevPersistentState !== newPersistentState) { - return null; + return false; } const prevInitDataTypes = prevConfiguration.initDataTypes ?? []; const newInitDataTypes = newConfiguration.initDataTypes ?? []; if (!isArraySubsetOf(newInitDataTypes, prevInitDataTypes)) { - return null; + return false; } const prevSessionTypes = prevConfiguration.sessionTypes ?? []; const newSessionTypes = newConfiguration.sessionTypes ?? []; if (!isArraySubsetOf(newSessionTypes, prevSessionTypes)) { - return null; + return false; } for (const prop of ["audioCapabilities", "videoCapabilities"] as const) { @@ -148,11 +151,11 @@ function checkCachedMediaKeySystemAccess( return false; }); if (!wasFound) { - return null; + return false; } } - return currentKeySystemAccess; + return true; } /** @@ -469,35 +472,32 @@ export default function getMediaKeySystemAccess( const currentState = await MediaKeysAttacher.getAttachedMediaKeysState(mediaElement); for (let configIdx = 0; configIdx < keySystemConfigurations.length; configIdx++) { const keySystemConfiguration = keySystemConfigurations[configIdx]; - if (currentState !== null) { - if ( - // TODO Do it with MediaKeySystemAccess.prototype.keySystem instead - keyType === currentState.keySystemOptions.type && - eme.implementation === currentState.emeImplementation.implementation - ) { - // Fast way to find a compatible keySystem if the currently loaded - // one as exactly the same compatibility options. - const cachedKeySystemAccess = checkCachedMediaKeySystemAccess( - keySystemConfiguration, - currentState.askedConfiguration, - currentState.mediaKeySystemAccess, - ); - if (cachedKeySystemAccess !== null) { - log.info("DRM: Found cached compatible keySystem"); - return Promise.resolve({ - type: "reuse-media-key-system-access" as const, - value: { - mediaKeySystemAccess: cachedKeySystemAccess, - askedConfiguration: currentState.askedConfiguration, - options: currentState.keySystemOptions, - codecSupport: extractCodecSupportListFromConfiguration( - currentState.askedConfiguration, - cachedKeySystemAccess.getConfiguration(), - ), - }, - }); - } - } + + // Check if the current `MediaKeySystemAccess` created cannot be reused here + if ( + currentState !== null && + !shouldRenewMediaKeySystemAccess() && + // TODO: Do it with MediaKeySystemAccess.prototype.keySystem instead? + keyType === currentState.keySystemOptions.type && + eme.implementation === currentState.emeImplementation.implementation && + isNewMediaKeySystemConfigurationCompatibleWithPreviousOne( + keySystemConfiguration, + currentState.askedConfiguration, + ) + ) { + log.info("DRM: Found cached compatible keySystem"); + return Promise.resolve({ + type: "reuse-media-key-system-access" as const, + value: { + mediaKeySystemAccess: currentState.mediaKeySystemAccess, + askedConfiguration: currentState.askedConfiguration, + options: currentState.keySystemOptions, + codecSupport: extractCodecSupportListFromConfiguration( + currentState.askedConfiguration, + currentState.mediaKeySystemAccess.getConfiguration(), + ), + }, + }); } try {