From ce6a11fd0a9ecdcd2d27ede3d3d4b056ef0f47f1 Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Thu, 21 Nov 2024 15:50:25 +0100 Subject: [PATCH] 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 | 154 +++++++++++++-------- 1 file changed, 97 insertions(+), 57 deletions(-) diff --git a/src/main_thread/decrypt/find_key_system.ts b/src/main_thread/decrypt/find_key_system.ts index dfe573bb04..a75cfce840 100644 --- a/src/main_thread/decrypt/find_key_system.ts +++ b/src/main_thread/decrypt/find_key_system.ts @@ -88,52 +88,75 @@ 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, - }; + if (newConfiguration.persistentState !== prevConfiguration.persistentState) { + return null; + } + + 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 +467,6 @@ export default function getMediaKeySystemAccess( } const chosenType = keySystemsType[index]; - - const currentState = MediaKeysInfosStore.getState(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); @@ -486,6 +479,38 @@ export default function getMediaKeySystemAccess( let keySystemAccess; for (let configIdx = 0; configIdx < keySystemConfigurations.length; configIdx++) { const keySystemConfiguration = keySystemConfigurations[configIdx]; + const currentState = MediaKeysInfosStore.getState(mediaElement); + 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 +569,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; +}