diff --git a/src/controller/eme-controller.ts b/src/controller/eme-controller.ts index b181c513177..a0ca395ff0a 100644 --- a/src/controller/eme-controller.ts +++ b/src/controller/eme-controller.ts @@ -535,147 +535,185 @@ class EMEController extends Logger implements ComponentAPI { return; } - let keyId: Uint8Array | null | undefined; - let keySystemDomain: KeySystems | undefined; - - if ( - initDataType === 'sinf' && - this.getLicenseServerUrl(KeySystems.FAIRPLAY) - ) { - // Match sinf keyId to playlist skd://keyId= - const json = bin2str(new Uint8Array(initData)); - try { - const sinf = base64Decode(JSON.parse(json).sinf); - const tenc = parseSinf(sinf); - if (!tenc) { - throw new Error( - `'schm' box missing or not cbcs/cenc with schi > tenc`, - ); - } - keyId = tenc.subarray(8, 24); - keySystemDomain = KeySystems.FAIRPLAY; - } catch (error) { - this.warn(`${logMessage} Failed to parse sinf: ${error}`); - return; - } - } else if (this.getLicenseServerUrl(KeySystems.WIDEVINE)) { - // Support Widevine clear-lead key-session creation (otherwise depend on playlist keys) - const psshResults = parseMultiPssh(initData); - - // TODO: If using keySystemAccessPromises we might want to wait until one is resolved + if (!this.keyFormatPromise) { let keySystems = Object.keys( this.keySystemAccessPromises, ) as KeySystems[]; if (!keySystems.length) { keySystems = getKeySystemsForConfig(this.config); } + const keyFormats = keySystems + .map(keySystemToKeySystemFormat) + .filter((k) => !!k) as KeySystemFormats[]; + this.keyFormatPromise = this.getKeyFormatPromise(keyFormats); + } - const psshInfo = psshResults.filter((pssh): pssh is PsshData => { - const keySystem = pssh.systemId - ? keySystemIdToKeySystemDomain(pssh.systemId) - : null; - return keySystem ? keySystems.indexOf(keySystem) > -1 : false; - })[0]; + this.keyFormatPromise.then((keySystemFormat) => { + const keySystem = keySystemFormatToKeySystemDomain(keySystemFormat); - if (!psshInfo) { + let keyId: Uint8Array | null | undefined; + let keySystemDomain: KeySystems | undefined; + + if (initDataType === 'sinf') { + if (keySystem !== KeySystems.FAIRPLAY) { + this.warn( + `Ignoring unexpected "${event.type}" event with init data type: "${initDataType}" for selected key-system ${keySystem}`, + ); + return; + } + // Match sinf keyId to playlist skd://keyId= + const json = bin2str(new Uint8Array(initData)); + try { + const sinf = base64Decode(JSON.parse(json).sinf); + const tenc = parseSinf(sinf); + if (!tenc) { + throw new Error( + `'schm' box missing or not cbcs/cenc with schi > tenc`, + ); + } + keyId = tenc.subarray(8, 24); + keySystemDomain = KeySystems.FAIRPLAY; + } catch (error) { + this.warn(`${logMessage} Failed to parse sinf: ${error}`); + return; + } + } else { if ( - psshResults.length === 0 || - psshResults.some((pssh): pssh is PsshInvalidResult => !pssh.systemId) + keySystem !== KeySystems.WIDEVINE && + keySystem !== KeySystems.PLAYREADY ) { - this.warn(`${logMessage} contains incomplete or invalid pssh data`); - } else { - this.log( - `ignoring ${logMessage} for ${(psshResults as PsshData[]) - .map((pssh) => keySystemIdToKeySystemDomain(pssh.systemId)) - .join(',')} pssh data in favor of playlist keys`, + this.warn( + `Ignoring unexpected "${event.type}" event with init data type: "${initDataType}" for selected key-system ${keySystem}`, ); + return; } - return; - } + // Support Widevine/PlayReady clear-lead key-session creation (otherwise depend on playlist keys) + const psshResults = parseMultiPssh(initData); - keySystemDomain = keySystemIdToKeySystemDomain(psshInfo.systemId); - if (psshInfo.version === 0 && psshInfo.data) { - if (keySystemDomain === KeySystems.WIDEVINE) { - const offset = psshInfo.data.length - 22; - keyId = psshInfo.data.subarray(offset, offset + 16); - } else if (keySystemDomain === KeySystems.PLAYREADY) { - keyId = parsePlayReadyWRM(psshInfo.data); + const psshInfos = psshResults.filter( + (pssh): pssh is PsshData => + !!pssh.systemId && + keySystemIdToKeySystemDomain(pssh.systemId) === keySystem, + ); + + if (psshInfos.length) { + this.warn( + `${logMessage} Using first of ${psshInfos.length} pssh found for selected key-system ${keySystem}`, + ); } - } - } - if (!keySystemDomain || !keyId) { - return; - } + const psshInfo = psshInfos[0]; - const keyIdHex = Hex.hexDump(keyId); - const { keyIdToKeySessionPromise, mediaKeySessions } = this; + if (!psshInfo) { + if ( + psshResults.length === 0 || + psshResults.some( + (pssh): pssh is PsshInvalidResult => !pssh.systemId, + ) + ) { + this.warn(`${logMessage} contains incomplete or invalid pssh data`); + } else { + this.log( + `ignoring ${logMessage} for ${(psshResults as PsshData[]) + .map((pssh) => keySystemIdToKeySystemDomain(pssh.systemId)) + .join(',')} pssh data in favor of playlist keys`, + ); + } + return; + } - let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex]; - for (let i = 0; i < mediaKeySessions.length; i++) { - // Match playlist key - const keyContext = mediaKeySessions[i]; - const decryptdata = keyContext.decryptdata; - if (!decryptdata.keyId) { - continue; - } - const oldKeyIdHex = Hex.hexDump(decryptdata.keyId); - if ( - keyIdHex === oldKeyIdHex || - decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1 - ) { - keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex]; - if (decryptdata.pssh) { - break; + keySystemDomain = keySystemIdToKeySystemDomain(psshInfo.systemId); + if (psshInfo.version === 0 && psshInfo.data) { + if (keySystemDomain === KeySystems.WIDEVINE) { + const offset = psshInfo.data.length - 22; + keyId = psshInfo.data.subarray(offset, offset + 16); + } else if (keySystemDomain === KeySystems.PLAYREADY) { + keyId = parsePlayReadyWRM(psshInfo.data); + } } - delete keyIdToKeySessionPromise[oldKeyIdHex]; - decryptdata.pssh = new Uint8Array(initData); - decryptdata.keyId = keyId; - keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] = - keySessionContextPromise.then(() => { - return this.generateRequestWithPreferredKeySession( - keyContext, - initDataType, - initData, - 'encrypted-event-key-match', - ); - }); - keySessionContextPromise.catch((error) => this.handleError(error)); - break; } - } - if (!keySessionContextPromise) { - // Clear-lead key (not encountered in playlist) - keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] = - this.getKeySystemSelectionPromise([keySystemDomain]).then( - ({ keySystem, mediaKeys }) => { - this.throwIfDestroyed(); - const decryptdata = new LevelKey( - 'ISO-23001-7', - keyIdHex, - keySystemToKeySystemFormat(keySystem) ?? '', - ); - decryptdata.pssh = new Uint8Array(initData); - decryptdata.keyId = keyId as Uint8Array; - return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => { - this.throwIfDestroyed(); - const keySessionContext = this.createMediaKeySessionContext({ - decryptdata, - keySystem, - mediaKeys, - }); + if (!keySystemDomain || !keyId) { + return; + } + + const keyIdHex = Hex.hexDump(keyId); + const { keyIdToKeySessionPromise, mediaKeySessions } = this; + + let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex]; + for (let i = 0; i < mediaKeySessions.length; i++) { + // Match playlist key + const keyContext = mediaKeySessions[i]; + const decryptdata = keyContext.decryptdata; + if (!decryptdata.keyId) { + continue; + } + const oldKeyIdHex = Hex.hexDump(decryptdata.keyId); + if ( + keyIdHex === oldKeyIdHex || + decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1 + ) { + keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex]; + if (decryptdata.pssh) { + break; + } + delete keyIdToKeySessionPromise[oldKeyIdHex]; + decryptdata.pssh = new Uint8Array(initData); + decryptdata.keyId = keyId; + keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] = + keySessionContextPromise.then(() => { return this.generateRequestWithPreferredKeySession( - keySessionContext, + keyContext, initDataType, initData, - 'encrypted-event-no-match', + 'encrypted-event-key-match', ); }); - }, - ); - keySessionContextPromise.catch((error) => this.handleError(error)); - } + keySessionContextPromise.catch((error) => this.handleError(error)); + break; + } + } + + if (!keySessionContextPromise) { + if (keySystemDomain !== keySystem) { + this.log( + `Ignoring "${event.type}" event with ${keySystemDomain} init data for selected key-system ${keySystem}`, + ); + return; + } + // "Clear-lead" (misc key not encountered in playlist) + keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] = + this.getKeySystemSelectionPromise([keySystemDomain]).then( + ({ keySystem, mediaKeys }) => { + this.throwIfDestroyed(); + + const decryptdata = new LevelKey( + 'ISO-23001-7', + keyIdHex, + keySystemToKeySystemFormat(keySystem) ?? '', + ); + decryptdata.pssh = new Uint8Array(initData); + decryptdata.keyId = keyId as Uint8Array; + return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => { + this.throwIfDestroyed(); + const keySessionContext = this.createMediaKeySessionContext({ + decryptdata, + keySystem, + mediaKeys, + }); + return this.generateRequestWithPreferredKeySession( + keySessionContext, + initDataType, + initData, + 'encrypted-event-no-match', + ); + }); + }, + ); + + keySessionContextPromise.catch((error) => this.handleError(error)); + } + }); }; private onWaitingForKey = (event: Event) => { diff --git a/src/loader/key-loader.ts b/src/loader/key-loader.ts index 82ab889ba6a..5042a944d52 100644 --- a/src/loader/key-loader.ts +++ b/src/loader/key-loader.ts @@ -112,7 +112,12 @@ export default class KeyLoader implements ComponentAPI { } load(frag: Fragment): Promise { - if (!frag.decryptdata && frag.encrypted && this.emeController) { + if ( + !frag.decryptdata && + frag.encrypted && + this.emeController && + this.config.emeEnabled + ) { // Multiple keys, but none selected, resolve in eme-controller return this.emeController .selectKeySystemFormat(frag) diff --git a/tests/unit/controller/eme-controller.ts b/tests/unit/controller/eme-controller.ts index b8f3152266d..c00eea18861 100644 --- a/tests/unit/controller/eme-controller.ts +++ b/tests/unit/controller/eme-controller.ts @@ -5,6 +5,7 @@ import sinonChai from 'sinon-chai'; import EMEController from '../../../src/controller/eme-controller'; import { ErrorDetails } from '../../../src/errors'; import { Events } from '../../../src/events'; +import { KeySystemFormats } from '../../../src/utils/mediakeys-helper'; import HlsMock from '../../mocks/hls.mock'; import type { MediaKeySessionContext } from '../../../src/controller/eme-controller'; import type { MediaAttachedData } from '../../../src/types/events'; @@ -266,6 +267,11 @@ describe('EMEController', function () { setupEach({ emeEnabled: true, requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy, + drmSystems: { + 'com.apple.fps': { + licenseUrl: '.', + }, + }, }); const badData = { @@ -293,10 +299,22 @@ describe('EMEController', function () { media.emit('encrypted', badData); - expect(emeController.keyIdToKeySessionPromise).to.deep.equal( - {}, - '`keyIdToKeySessionPromise` should be an empty dictionary when no key IDs are found', - ); + return emeController + .selectKeySystemFormat({ + levelkeys: { + [KeySystemFormats.FAIRPLAY]: {}, + [KeySystemFormats.WIDEVINE]: {}, + [KeySystemFormats.PLAYREADY]: {}, + }, + sn: 0, + type: 'main', + } as any) + .then(() => { + expect(emeController.keyIdToKeySessionPromise).to.deep.equal( + {}, + '`keyIdToKeySessionPromise` should be an empty dictionary when no key IDs are found', + ); + }); }); it('should fetch the server certificate and set it into the session', function () {