diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 8bd2e95ed2e..5c6627d65a4 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -1104,7 +1104,7 @@ export const enum DecrypterAesMode { export type DRMSystemConfiguration = { licenseUrl: string; serverCertificateUrl?: string; - generateRequest?: (this: Hls, initDataType: string, initData: ArrayBuffer | null, keyContext: MediaKeySessionContext) => { + generateRequest?: (this: Hls, initDataType: string, initData: ArrayBuffer | null, keyContext: MediaKeySessionContextAndLevelKey) => { initDataType: string; initData: ArrayBuffer | null; } | undefined | never; @@ -1189,8 +1189,8 @@ export class EMEController extends Logger implements ComponentAPI { // // @public (undocumented) export type EMEControllerConfig = { - licenseXhrSetup?: (this: Hls, xhr: XMLHttpRequest, url: string, keyContext: MediaKeySessionContext, licenseChallenge: Uint8Array) => void | Uint8Array | Promise; - licenseResponseCallback?: (this: Hls, xhr: XMLHttpRequest, url: string, keyContext: MediaKeySessionContext) => ArrayBuffer; + licenseXhrSetup?: (this: Hls, xhr: XMLHttpRequest, url: string, keyContext: MediaKeySessionContextAndLevelKey, licenseChallenge: Uint8Array) => void | Uint8Array | Promise; + licenseResponseCallback?: (this: Hls, xhr: XMLHttpRequest, url: string, keyContext: MediaKeySessionContextAndLevelKey) => ArrayBuffer; emeEnabled: boolean; widevineLicenseUrl?: string; drmSystems: DRMSystemsConfiguration | undefined; @@ -3361,7 +3361,9 @@ export class LevelKey implements DecryptData { // (undocumented) readonly encrypted: boolean; // (undocumented) - getDecryptData(sn: number | 'initSegment'): LevelKey | null; + getDecryptData(sn: number | 'initSegment', levelKeys?: { + [key: string]: LevelKey | undefined; + }): LevelKey | null; // (undocumented) readonly isCommonEncryption: boolean; // (undocumented) @@ -3996,9 +3998,9 @@ export type MediaKeyFunc = (keySystem: KeySystems, supportedConfigurations: Medi // @public (undocumented) export interface MediaKeySessionContext { // (undocumented) - decryptdata: LevelKey; - // (undocumented) - keyStatus?: MediaKeyStatus; + keyStatuses: { + [keyId: string]: MediaKeyStatus; + }; // (undocumented) keyStatusTimeouts?: { [keyId: string]: number; @@ -4006,6 +4008,8 @@ export interface MediaKeySessionContext { // (undocumented) keySystem: KeySystems; // (undocumented) + levelKeys: LevelKey[]; + // (undocumented) licenseXhr?: XMLHttpRequest; // (undocumented) mediaKeys: MediaKeys; @@ -4017,6 +4021,24 @@ export interface MediaKeySessionContext { _onmessage?: (this: MediaKeySession, ev: MediaKeyMessageEvent) => any; } +// Warning: (ae-missing-release-tag) "MediaKeySessionContextAndLevelKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type MediaKeySessionContextAndLevelKey = { + decryptdata: LevelKey; + keySystem: KeySystems; + levelKeys: LevelKey[]; + mediaKeys: MediaKeys; + mediaKeysSession: MediaKeySession; + keyStatuses: { + [keyId: string]: MediaKeyStatus; + }; + keyStatusTimeouts?: { + [keyId: string]: number; + }; + licenseXhr?: XMLHttpRequest; +}; + // Warning: (ae-missing-release-tag) "MediaOverrides" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/config.ts b/src/config.ts index 7ba0901fb2f..553f3e4a89d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,7 +17,7 @@ import FetchLoader, { fetchSupported } from './utils/fetch-loader'; import { requestMediaKeySystemAccess } from './utils/mediakeys-helper'; import { stringify } from './utils/safe-json-stringify'; import XhrLoader from './utils/xhr-loader'; -import type { MediaKeySessionContext } from './controller/eme-controller'; +import type { MediaKeySessionContextAndLevelKey } from './controller/eme-controller'; import type Hls from './hls'; import type { FragmentLoaderContext, @@ -92,7 +92,7 @@ export type DRMSystemConfiguration = { this: Hls, initDataType: string, initData: ArrayBuffer | null, - keyContext: MediaKeySessionContext, + keyContext: MediaKeySessionContextAndLevelKey, ) => | { initDataType: string; initData: ArrayBuffer | null } | undefined @@ -108,14 +108,14 @@ export type EMEControllerConfig = { this: Hls, xhr: XMLHttpRequest, url: string, - keyContext: MediaKeySessionContext, + keyContext: MediaKeySessionContextAndLevelKey, licenseChallenge: Uint8Array, ) => void | Uint8Array | Promise; licenseResponseCallback?: ( this: Hls, xhr: XMLHttpRequest, url: string, - keyContext: MediaKeySessionContext, + keyContext: MediaKeySessionContextAndLevelKey, ) => ArrayBuffer; emeEnabled: boolean; widevineLicenseUrl?: string; diff --git a/src/controller/eme-controller.ts b/src/controller/eme-controller.ts index a4ea86f8854..33abc6acc23 100644 --- a/src/controller/eme-controller.ts +++ b/src/controller/eme-controller.ts @@ -56,16 +56,28 @@ interface KeySystemAccessPromises { export interface MediaKeySessionContext { keySystem: KeySystems; + levelKeys: LevelKey[]; mediaKeys: MediaKeys; - decryptdata: LevelKey; mediaKeysSession: MediaKeySession; - keyStatus?: MediaKeyStatus; + keyStatuses: { [keyId: string]: MediaKeyStatus }; keyStatusTimeouts?: { [keyId: string]: number }; licenseXhr?: XMLHttpRequest; _onmessage?: (this: MediaKeySession, ev: MediaKeyMessageEvent) => any; _onkeystatuseschange?: (this: MediaKeySession, ev: Event) => any; } +// TODO: Remove duplicate typing (interface -> type (type is event specific)) +export type MediaKeySessionContextAndLevelKey = { + decryptdata: LevelKey; // soft-deprecated (replaced by levelKeys) + keySystem: KeySystems; + levelKeys: LevelKey[]; + mediaKeys: MediaKeys; + mediaKeysSession: MediaKeySession; + keyStatuses: { [keyId: string]: MediaKeyStatus }; + keyStatusTimeouts?: { [keyId: string]: number }; + licenseXhr?: XMLHttpRequest; +}; + /** * Controller to deal with encrypted media extensions (EME) * @see https://developer.mozilla.org/en-US/docs/Web/API/Encrypted_Media_Extensions_API @@ -89,14 +101,18 @@ class EMEController extends Logger implements ComponentAPI { } = {}; private _requestLicenseFailureCount: number = 0; private mediaKeySessions: MediaKeySessionContext[] = []; - private keyIdToKeySessionPromise: { - [keyId: string]: Promise | undefined; + // TODO: keyUsablePromises (per KeyId, resolves on (startsWith) "usable", deleted when changed to expired or released) + // TODO: One session with updates, or session queue for playlist (disco, timerange, etc...) and key rotation context? + private keyUriToSessionPromise: { + [keyUri: string]: Promise | undefined; + } = {}; + private keyUriToLevelKeys: { + [keyUri: string]: LevelKey[] | undefined; } = {}; private mediaKeys: MediaKeys | null = null; private setMediaKeysQueue: Promise[] = EMEController.CDMCleanupPromise ? [EMEController.CDMCleanupPromise] : []; - private bannedKeyIds: { [keyId: string]: MediaKeyStatus | undefined } = {}; constructor(hls: Hls) { super('eme', hls.logger); @@ -114,7 +130,9 @@ class EMEController extends Logger implements ComponentAPI { config.licenseXhrSetup = config.licenseResponseCallback = undefined; config.drmSystems = config.drmSystemOptions = {}; // @ts-ignore - this.hls = this.config = this.keyIdToKeySessionPromise = null; + this.hls = this.config = null; + // @ts-ignore + this.keyUriToSessionPromise = this.keyUriToLevelKeys = null; // @ts-ignore this.onMediaEncrypted = this.onWaitingForKey = null; } @@ -312,15 +330,18 @@ class EMEController extends Logger implements ComponentAPI { return keySystemAccess.then(() => keySystemAccessPromises!.mediaKeys!); } - private createMediaKeySessionContext({ - decryptdata, - keySystem, - mediaKeys, - }: { - decryptdata: LevelKey; - keySystem: KeySystems; - mediaKeys: MediaKeys; - }): MediaKeySessionContext { + private createMediaKeySessionContext( + { + levelKeys, + keySystem, + mediaKeys, + }: { + levelKeys: LevelKey[]; + keySystem: KeySystems; + mediaKeys: MediaKeys; + }, + decryptdata: LevelKey, + ): MediaKeySessionContext { this.log( `Creating key-system session "${keySystem}" keyId: ${arrayToHex( decryptdata.keyId || ([] as number[]), @@ -330,11 +351,11 @@ class EMEController extends Logger implements ComponentAPI { const mediaKeysSession = mediaKeys.createSession(); const mediaKeySessionContext: MediaKeySessionContext = { - decryptdata, + levelKeys, keySystem, mediaKeys, mediaKeysSession, - keyStatus: 'status-pending', + keyStatuses: {}, }; this.mediaKeySessions.push(mediaKeySessionContext); @@ -342,20 +363,24 @@ class EMEController extends Logger implements ComponentAPI { return mediaKeySessionContext; } - private renewKeySession(mediaKeySessionContext: MediaKeySessionContext) { - const decryptdata = mediaKeySessionContext.decryptdata; - if (decryptdata.pssh) { + private renewKeySession( + mediaKeySessionContext: MediaKeySessionContext, + levelKey: LevelKey, + ) { + if (levelKey.pssh) { const keySessionContext = this.createMediaKeySessionContext( mediaKeySessionContext, + levelKey, ); - const keyId = getKeyIdString(decryptdata); + const keyUri = levelKey.uri; const scheme = 'cenc'; - this.keyIdToKeySessionPromise[keyId] = + this.keyUriToSessionPromise[keyUri] = this.generateRequestWithPreferredKeySession( keySessionContext, scheme, - decryptdata.pssh.buffer, + levelKey.pssh.buffer, 'expired', + levelKey, ); } else { this.warn(`Could not renew expired session. Missing pssh initData.`); @@ -366,12 +391,13 @@ class EMEController extends Logger implements ComponentAPI { private updateKeySession( mediaKeySessionContext: MediaKeySessionContext, + decryptdata: LevelKey, data: Uint8Array, ): Promise { const keySession = mediaKeySessionContext.mediaKeysSession; this.log( `Updating key-session "${keySession.sessionId}" for keyId ${arrayToHex( - mediaKeySessionContext.decryptdata.keyId || [], + decryptdata.keyId || [], )} } (data length: ${data.byteLength})`, ); @@ -454,27 +480,35 @@ class EMEController extends Logger implements ComponentAPI { } public loadKey(data: KeyLoadedData): Promise { - const decryptdata = data.keyInfo.decryptdata; - - const keyId = getKeyIdString(decryptdata); - const badStatus = this.bannedKeyIds[keyId]; - if (badStatus || this.getKeyStatus(decryptdata) === 'internal-error') { - const error = getKeyStatusError( - badStatus || 'internal-error', - decryptdata, - ); + const levelKey = data.keyInfo.decryptdata; + + // Error immediately when encountering a key ID with this status again + const status = this.getKeyStatus(levelKey); + if (status === 'internal-error') { + const error = getKeyStatusError(status, levelKey); this.handleError(error, data.frag); return Promise.reject(error); } - const keyDetails = `(keyId: ${keyId} format: "${decryptdata.keyFormat}" method: ${decryptdata.method} uri: ${decryptdata.uri})`; - this.log(`Starting session for key ${keyDetails}`); + const keyUri = levelKey.uri; + + // Keep collection of level-keys associated with Key URI and key-session context + let levelKeys = this.keyUriToLevelKeys[keyUri]; + if (!levelKeys) { + levelKeys = this.keyUriToLevelKeys[keyUri] = [levelKey]; + } else if ( + !levelKeys.some((decryptdata) => decryptdata.matches(levelKey)) + ) { + levelKeys.push(levelKey); + } - const keyContextPromise = this.keyIdToKeySessionPromise[keyId]; + // Get key-session context async + const keyContextPromise = this.keyUriToSessionPromise[keyUri]; if (!keyContextPromise) { - const keySessionContextPromise = this.getKeySystemForKeyPromise( - decryptdata, - ) + const keyId = getKeyIdString(levelKey); + const keyDetails = `(keyId: ${keyId} URI: ${keyUri} format: "${levelKey.keyFormat}" method: ${levelKey.method})`; + this.log(`Starting session for key ${keyDetails}`); + const keySessionContextPromise = this.getKeySystemForKeyPromise(levelKey) .then(({ keySystem, mediaKeys }) => { this.throwIfDestroyed(); this.log( @@ -483,28 +517,32 @@ class EMEController extends Logger implements ComponentAPI { return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => { this.throwIfDestroyed(); - return this.createMediaKeySessionContext({ - keySystem, - mediaKeys, - decryptdata, - }); + return this.createMediaKeySessionContext( + { + keySystem, + mediaKeys, + levelKeys, + }, + levelKey, + ); }); }) .then((keySessionContext) => { const scheme = 'cenc'; - const initData = decryptdata.pssh ? decryptdata.pssh.buffer : null; + const initData = levelKey.pssh ? levelKey.pssh.buffer : null; return this.generateRequestWithPreferredKeySession( keySessionContext, scheme, initData, 'playlist-key', + levelKey, ); }); keySessionContextPromise.catch((error) => this.handleError(error, data.frag), ); - this.keyIdToKeySessionPromise[keyId] = keySessionContextPromise; + this.keyUriToSessionPromise[keyUri] = keySessionContextPromise; return keySessionContextPromise; } @@ -513,8 +551,8 @@ class EMEController extends Logger implements ComponentAPI { keyContextPromise.catch((error) => { if (error instanceof EMEKeyError) { const errorData = { ...error.data }; - if (this.getKeyStatus(decryptdata) === 'internal-error') { - errorData.decryptdata = decryptdata; + if (this.getKeyStatus(levelKey) === 'internal-error') { + errorData.decryptdata = levelKey; } const clonedError = new EMEKeyError(errorData, error.message); this.handleError(clonedError, data.frag); @@ -560,8 +598,8 @@ class EMEController extends Logger implements ComponentAPI { private getKeySystemForKeyPromise( decryptdata: LevelKey, ): Promise<{ keySystem: KeySystems; mediaKeys: MediaKeys }> { - const keyId = getKeyIdString(decryptdata); - const mediaKeySessionContext = this.keyIdToKeySessionPromise[keyId]; + const keyUri = decryptdata.uri; + const mediaKeySessionContext = this.keyUriToSessionPromise[keyUri]; if (!mediaKeySessionContext) { const keySystem = keySystemFormatToKeySystemDomain( decryptdata.keyFormat as KeySystemFormats, @@ -646,42 +684,49 @@ class EMEController extends Logger implements ComponentAPI { } const keyIdHex = arrayToHex(keyId); - const { keyIdToKeySessionPromise, mediaKeySessions } = this; - let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex]; + const { keyUriToSessionPromise, mediaKeySessions } = this; + let keySessionContextPromise: + | Promise + | undefined; + // Match `tenc` box keyId to playlist key in session 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 = arrayToHex(decryptdata.keyId); - if ( - arrayValuesMatch(keyId, decryptdata.keyId) || - decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1 - ) { - keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex]; - if (!keySessionContextPromise) { + for (let j = 0; j < keyContext.levelKeys.length; j++) { + const decryptdata = keyContext.levelKeys[j]; + if (!decryptdata.keyId) { continue; } - if (decryptdata.pssh) { + const sessionKeyUri = decryptdata.uri; + if ( + arrayValuesMatch(keyId, decryptdata.keyId) || + sessionKeyUri.replace(/-/g, '').indexOf(keyIdHex) !== -1 + ) { + keySessionContextPromise = keyUriToSessionPromise[sessionKeyUri]; + if (!keySessionContextPromise) { + continue; + } + if (decryptdata.pssh) { + break; + } + decryptdata.pssh = new Uint8Array(initData); + decryptdata.keyId = keyId; + LevelKey.setKeyIdForUri(sessionKeyUri, keyId); + keySessionContextPromise = keyUriToSessionPromise[sessionKeyUri] = + keySessionContextPromise.then(() => { + return this.generateRequestWithPreferredKeySession( + keyContext, + initDataType, + initData, + 'encrypted-event-key-match', + decryptdata, + ); + }); + keySessionContextPromise.catch((error) => + this.handleError(error), + ); break; } - 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; } } @@ -741,13 +786,22 @@ class EMEController extends Logger implements ComponentAPI { | 'encrypted-event-key-match' | 'encrypted-event-no-match' | 'expired', + levelKey: LevelKey, ): Promise | never { const generateRequestFilter = this.config.drmSystems?.[context.keySystem]?.generateRequest; if (generateRequestFilter) { + const contextWithLevelKey = Object.assign({}, context, { + decryptdata: levelKey, + }); try { const mappedInitData: ReturnType = - generateRequestFilter.call(this.hls, initDataType, initData, context); + generateRequestFilter.call( + this.hls, + initDataType, + initData, + contextWithLevelKey, + ); if (!mappedInitData) { throw new Error( 'Invalid response from configured generateRequest filter', @@ -755,7 +809,7 @@ class EMEController extends Logger implements ComponentAPI { } initDataType = mappedInitData.initDataType; initData = mappedInitData.initData ? mappedInitData.initData : null; - context.decryptdata.pssh = initData ? new Uint8Array(initData) : null; + levelKey.pssh = initData ? new Uint8Array(initData) : null; } catch (error) { this.warn(error.message); if ((this.hls as any) && this.hls.config.debug) { @@ -769,8 +823,8 @@ class EMEController extends Logger implements ComponentAPI { return Promise.resolve(context); } - const keyId = getKeyIdString(context.decryptdata); - const keyUri = context.decryptdata.uri; + const keyId = getKeyIdString(levelKey); + const keyUri = levelKey.uri; this.log( `Generating key-session request for "${reason}" keyId: ${keyId} URI: ${keyUri} (init data type: ${initDataType} length: ${ initData.byteLength @@ -793,16 +847,22 @@ class EMEController extends Logger implements ComponentAPI { messageType === 'license-request' || messageType === 'license-renewal' ) { - this.renewLicense(context, message).catch((error) => { + this.renewLicense(context, levelKey, message).catch((error) => { if (licenseStatus.eventNames().length) { licenseStatus.emit('error', error); } else { this.handleError(error); } + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.removeSession(context); }); } else if (messageType === 'license-release') { if (context.keySystem === KeySystems.FAIRPLAY) { - this.updateKeySession(context, strToUtf8array('acknowledged')) + this.updateKeySession( + context, + levelKey, + strToUtf8array('acknowledged'), + ) .then(() => this.removeSession(context)) .catch((error) => this.handleError(error)); } @@ -811,11 +871,7 @@ class EMEController extends Logger implements ComponentAPI { } }); - const handleKeyStatus = ( - keyStatus: MediaKeyStatus, - context: MediaKeySessionContext, - ) => { - context.keyStatus = keyStatus; + const handleKeyStatus = (keyStatus: MediaKeyStatus) => { let keyError: EMEKeyError | Error | undefined; if (keyStatus.startsWith('usable')) { licenseStatus.emit('resolved'); @@ -824,7 +880,7 @@ class EMEController extends Logger implements ComponentAPI { keyStatus === 'output-restricted' || keyStatus === 'output-downscaled' ) { - keyError = getKeyStatusError(keyStatus, context.decryptdata); + keyError = getKeyStatusError(keyStatus, levelKey); // TODO: levelKeys (which one?) } else if (keyStatus === 'expired') { keyError = new Error(`key expired (keyId: ${keyId})`); } else if (keyStatus === 'released') { @@ -844,6 +900,7 @@ class EMEController extends Logger implements ComponentAPI { } } }; + const onkeystatuseschange = (context._onkeystatuseschange = ( event: Event, ) => { @@ -856,41 +913,41 @@ class EMEController extends Logger implements ComponentAPI { const keyStatuses = this.getKeyStatuses(context); const keyIds = Object.keys(keyStatuses); - // exit if all keys are status-pending + // ignore change if all keys are status-pending if (!keyIds.some((id) => keyStatuses[id] !== 'status-pending')) { return; } - // renew when a key status for a levelKey comes back expired + // TODO: If any of the active level keys are expired then renew + // if (keyIds.some((id) => context.keyStatuses[id] === 'expired')) { if (keyStatuses[keyId] === 'expired') { // renew when a key status comes back expired this.log( `Expired key ${stringify(keyStatuses)} in key-session "${context.mediaKeysSession.sessionId}"`, ); - this.renewKeySession(context); - return; + this.renewKeySession(context, levelKey); } let keyStatus = keyStatuses[keyId] as MediaKeyStatus | undefined; if (keyStatus) { // handle status of current key - handleKeyStatus(keyStatus, context); + handleKeyStatus(keyStatus); } else { // Timeout key-status - const timeout = 0; + const timeout = 1000; context.keyStatusTimeouts ||= {}; context.keyStatusTimeouts[keyId] ||= self.setTimeout(() => { - if ((!context.mediaKeysSession as any) || !this.mediaKeys) { + if ((!context.mediaKeysSession as any) || !context.levelKeys.length) { return; } // Find key status in another session if missing (PlayReady #7519 no key-status "single-key" setup with shared key) - const sessionKeyStatus = this.getKeyStatus(context.decryptdata); + const sessionKeyStatus = this.getKeyStatus(levelKey); if (sessionKeyStatus && sessionKeyStatus !== 'status-pending') { this.log( `No status for keyId ${keyId} in key-session "${context.mediaKeysSession.sessionId}". Using session key-status ${sessionKeyStatus} from other session.`, ); - return handleKeyStatus(sessionKeyStatus, context); + return handleKeyStatus(sessionKeyStatus); } // Timeout key with internal-error @@ -898,7 +955,7 @@ class EMEController extends Logger implements ComponentAPI { `key status for ${keyId} in key-session "${context.mediaKeysSession.sessionId}" timed out after ${timeout}ms`, ); keyStatus = 'internal-error'; - handleKeyStatus(keyStatus, context); + handleKeyStatus(keyStatus); }, timeout); this.log(`No status for keyId ${keyId} (${stringify(keyStatuses)}).`); @@ -932,7 +989,7 @@ class EMEController extends Logger implements ComponentAPI { type: ErrorTypes.KEY_SYSTEM_ERROR, details: ErrorDetails.KEY_SYSTEM_NO_SESSION, error, - decryptdata: context.decryptdata, + decryptdata: levelKey, fatal: false, }, `Error generating key-session request: ${error}`, @@ -975,10 +1032,6 @@ class EMEController extends Logger implements ComponentAPI { changeEndianness(keyIdArray); } const keyIdWithStatusChange = arrayToHex(keyIdArray); - // Add to banned keys to prevent playlist usage and license requests - if (status === 'internal-error') { - this.bannedKeyIds[keyIdWithStatusChange] = status; - } this.log( `key status change "${status}" for keyStatuses keyId: ${keyIdWithStatusChange} key-session "${mediaKeySessionContext.mediaKeysSession.sessionId}"`, ); @@ -1096,26 +1149,33 @@ class EMEController extends Logger implements ComponentAPI { private renewLicense( context: MediaKeySessionContext, + levelKey: LevelKey, keyMessage: ArrayBuffer, ): Promise { - return this.requestLicense(context, new Uint8Array(keyMessage)).then( - (data: ArrayBuffer) => { - return this.updateKeySession(context, new Uint8Array(data)).catch( - (error) => { - throw new EMEKeyError( - { - type: ErrorTypes.KEY_SYSTEM_ERROR, - details: ErrorDetails.KEY_SYSTEM_SESSION_UPDATE_FAILED, - decryptdata: context.decryptdata, - error, - fatal: false, - }, - error.message, - ); + const contextWithLevelKey = Object.assign({}, context, { + decryptdata: levelKey, + }); + return this.requestLicense( + contextWithLevelKey, + new Uint8Array(keyMessage), + ).then((data: ArrayBuffer) => { + return this.updateKeySession( + context, + levelKey, + new Uint8Array(data), + ).catch((error) => { + throw new EMEKeyError( + { + type: ErrorTypes.KEY_SYSTEM_ERROR, + details: ErrorDetails.KEY_SYSTEM_SESSION_UPDATE_FAILED, + decryptdata: levelKey, + error, + fatal: false, }, + error.message, ); - }, - ); + }); + }); } private unpackPlayReadyKeyMessage( @@ -1167,7 +1227,7 @@ class EMEController extends Logger implements ComponentAPI { private setupLicenseXHR( xhr: XMLHttpRequest, url: string, - keysListItem: MediaKeySessionContext, + contextWithLevelKey: MediaKeySessionContextAndLevelKey, licenseChallenge: Uint8Array, ): Promise<{ xhr: XMLHttpRequest; @@ -1183,19 +1243,19 @@ class EMEController extends Logger implements ComponentAPI { return Promise.resolve() .then(() => { - if (!keysListItem.decryptdata as any) { - throw new Error('Key removed'); + if (contextWithLevelKey.levelKeys.length === 0) { + throw new Error('Keys removed'); } return licenseXhrSetup.call( this.hls, xhr, url, - keysListItem, + contextWithLevelKey, licenseChallenge, ); }) .catch((error: Error) => { - if (!keysListItem.decryptdata as any) { + if (contextWithLevelKey.levelKeys.length === 0) { // Key session removed. Cancel license request. throw error; } @@ -1206,7 +1266,7 @@ class EMEController extends Logger implements ComponentAPI { this.hls, xhr, url, - keysListItem, + contextWithLevelKey, licenseChallenge, ); }) @@ -1223,19 +1283,21 @@ class EMEController extends Logger implements ComponentAPI { } private requestLicense( - keySessionContext: MediaKeySessionContext, + contextWithLevelKey: MediaKeySessionContextAndLevelKey, licenseChallenge: Uint8Array, ): Promise { const keyLoadPolicy = this.config.keyLoadPolicy.default; return new Promise((resolve, reject) => { - const url = this.getLicenseServerUrlOrThrow(keySessionContext.keySystem); + const url = this.getLicenseServerUrlOrThrow( + contextWithLevelKey.keySystem, + ); this.log(`Sending license request to URL: ${url}`); const xhr = new XMLHttpRequest(); xhr.responseType = 'arraybuffer'; xhr.onreadystatechange = () => { if ( (!this.hls as any) || - (!keySessionContext.mediaKeysSession as any) + (!contextWithLevelKey.mediaKeysSession as any) ) { return reject(new Error('invalid state')); } @@ -1255,7 +1317,7 @@ class EMEController extends Logger implements ComponentAPI { this.hls, xhr, url, - keySessionContext, + contextWithLevelKey, ); } catch (error) { this.error(error); @@ -1275,7 +1337,7 @@ class EMEController extends Logger implements ComponentAPI { { type: ErrorTypes.KEY_SYSTEM_ERROR, details: ErrorDetails.KEY_SYSTEM_LICENSE_REQUEST_FAILED, - decryptdata: keySessionContext.decryptdata, + decryptdata: contextWithLevelKey.decryptdata, fatal: true, networkDetails: xhr, response: { @@ -1294,7 +1356,7 @@ class EMEController extends Logger implements ComponentAPI { this.warn( `Retrying license request, ${attemptsLeft} attempts left`, ); - this.requestLicense(keySessionContext, licenseChallenge).then( + this.requestLicense(contextWithLevelKey, licenseChallenge).then( resolve, reject, ); @@ -1303,16 +1365,16 @@ class EMEController extends Logger implements ComponentAPI { } }; if ( - keySessionContext.licenseXhr && - keySessionContext.licenseXhr.readyState !== XMLHttpRequest.DONE + contextWithLevelKey.licenseXhr && + contextWithLevelKey.licenseXhr.readyState !== XMLHttpRequest.DONE ) { - keySessionContext.licenseXhr.abort(); + contextWithLevelKey.licenseXhr.abort(); } - keySessionContext.licenseXhr = xhr; + contextWithLevelKey.licenseXhr = xhr; - this.setupLicenseXHR(xhr, url, keySessionContext, licenseChallenge) + this.setupLicenseXHR(xhr, url, contextWithLevelKey, licenseChallenge) .then(({ xhr, licenseChallenge }) => { - if (keySessionContext.keySystem == KeySystems.PLAYREADY) { + if (contextWithLevelKey.keySystem == KeySystems.PLAYREADY) { licenseChallenge = this.unpackPlayReadyKeyMessage( xhr, licenseChallenge, @@ -1359,8 +1421,8 @@ class EMEController extends Logger implements ComponentAPI { private _clear() { this._requestLicenseFailureCount = 0; - this.keyIdToKeySessionPromise = {}; - this.bannedKeyIds = {}; + this.keyUriToSessionPromise = {}; + this.keyUriToLevelKeys = {}; if (!this.mediaKeys && !this.mediaKeySessions.length) { return; } @@ -1415,7 +1477,6 @@ class EMEController extends Logger implements ComponentAPI { private onManifestLoading() { this.keyFormatPromise = null; - this.bannedKeyIds = {}; } private onManifestLoaded( @@ -1447,11 +1508,10 @@ class EMEController extends Logger implements ComponentAPI { private removeSession( mediaKeySessionContext: MediaKeySessionContext, ): Promise { - const { mediaKeysSession, licenseXhr, decryptdata } = - mediaKeySessionContext; + const { mediaKeysSession, licenseXhr, levelKeys } = mediaKeySessionContext; if (mediaKeysSession as MediaKeySession | undefined) { this.log( - `Remove licenses and keys and close session "${mediaKeysSession.sessionId}" keyId: ${arrayToHex((decryptdata as LevelKey | undefined)?.keyId || [])}`, + `Remove licenses and keys and close session "${mediaKeysSession.sessionId}" keyIds: ${levelKeys.map(({ keyId }) => arrayToHex(keyId || []))} URI: ${levelKeys[0]?.uri}`, ); if (mediaKeySessionContext._onmessage) { mediaKeysSession.removeEventListener( @@ -1472,9 +1532,8 @@ class EMEController extends Logger implements ComponentAPI { licenseXhr.abort(); } mediaKeySessionContext.mediaKeysSession = - mediaKeySessionContext.decryptdata = - mediaKeySessionContext.licenseXhr = - undefined!; + mediaKeySessionContext.licenseXhr = undefined!; + mediaKeySessionContext.levelKeys = []; const index = this.mediaKeySessions.indexOf(mediaKeySessionContext); if (index > -1) { this.mediaKeySessions.splice(index, 1); @@ -1538,18 +1597,15 @@ function getKeyStatus( decryptdata: LevelKey, keyContext: MediaKeySessionContext, ): MediaKeyStatus | undefined { - if ( - decryptdata.keyId && - keyContext.mediaKeysSession.keyStatuses.has(decryptdata.keyId) - ) { - return keyContext.mediaKeysSession.keyStatuses.get(decryptdata.keyId); - } - if (decryptdata.matches(keyContext.decryptdata)) { - return keyContext.keyStatus; + const keyId = decryptdata.keyId; + if (keyId) { + if (keyContext.mediaKeysSession.keyStatuses.has(keyId)) { + return keyContext.mediaKeysSession.keyStatuses.get(keyId); + } + return keyContext.keyStatuses[arrayToHex(keyId)]; } return undefined; } - export class EMEKeyError extends Error { public readonly data: ErrorData; constructor( diff --git a/src/hls.ts b/src/hls.ts index cf61a67b01a..accd0c49ce2 100644 --- a/src/hls.ts +++ b/src/hls.ts @@ -1321,7 +1321,10 @@ export type { TimelineControllerConfig, TSDemuxerConfig, } from './config'; -export type { MediaKeySessionContext } from './controller/eme-controller'; +export type { + MediaKeySessionContext, + MediaKeySessionContextAndLevelKey, +} from './controller/eme-controller'; export type { FragmentState, FragmentTracker, diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts index 97e5e08cd95..621647eb563 100644 --- a/src/loader/fragment.ts +++ b/src/loader/fragment.ts @@ -259,25 +259,24 @@ export class Fragment extends BaseSegment { get decryptdata(): LevelKey | null { const { levelkeys } = this; - if (!levelkeys && !this._decryptdata) { + + if (!levelkeys || levelkeys.NONE) { return null; } - if (!this._decryptdata && this.levelkeys && !this.levelkeys.NONE) { - const key = this.levelkeys.identity; - if (key) { - this._decryptdata = key.getDecryptData(this.sn); - } else { - const keyFormats = Object.keys(this.levelkeys); - if (keyFormats.length === 1) { - const levelKey = (this._decryptdata = - this.levelkeys[keyFormats[0]] || null); - if (levelKey) { - return levelKey.getDecryptData(this.sn); - } - } else { - // Multiple keys. key-loader to call Fragment.setKeyFormat based on selected key-system. + if (levelkeys.identity) { + if (!this._decryptdata) { + this._decryptdata = levelkeys.identity.getDecryptData(this.sn); + } + } else if (!this._decryptdata?.keyId) { + const keyFormats = Object.keys(levelkeys); + if (keyFormats.length === 1) { + const levelKey = (this._decryptdata = levelkeys[keyFormats[0]] || null); + if (levelKey) { + this._decryptdata = levelKey.getDecryptData(this.sn, levelkeys); } + } else { + // Multiple keys. key-loader to call Fragment.setKeyFormat based on selected key-system. } } @@ -364,10 +363,11 @@ export class Fragment extends BaseSegment { } setKeyFormat(keyFormat: KeySystemFormats) { - if (this.levelkeys) { - const key = this.levelkeys[keyFormat]; - if (key && !this._decryptdata) { - this._decryptdata = key.getDecryptData(this.sn); + const levelkeys = this.levelkeys; + if (levelkeys) { + const key = levelkeys[keyFormat]; + if (key && !this._decryptdata?.keyId) { + this._decryptdata = key.getDecryptData(this.sn, levelkeys); } } } diff --git a/src/loader/key-loader.ts b/src/loader/key-loader.ts index 9d5b6556c1c..5a8a45252d0 100644 --- a/src/loader/key-loader.ts +++ b/src/loader/key-loader.ts @@ -1,4 +1,5 @@ import { LoadError } from './fragment-loader'; +import { LevelKey } from './level-key'; import { ErrorDetails, ErrorTypes } from '../errors'; import { type Fragment, isMediaFragment } from '../loader/fragment'; import { arrayToHex } from '../utils/hex'; @@ -8,7 +9,7 @@ import { keySystemFormatToKeySystemDomain, } from '../utils/mediakeys-helper'; import { KeySystemFormats } from '../utils/mediakeys-helper'; -import type { LevelKey } from './level-key'; +import { parseKeyIdsFromTenc } from '../utils/mp4-tools'; import type { HlsConfig } from '../config'; import type EMEController from '../controller/eme-controller'; import type { @@ -259,6 +260,19 @@ export default class KeyLoader extends Logger implements ComponentAPI { loadKeyEME(keyInfo: KeyLoaderInfo, frag: Fragment): Promise { const keyLoadedData: KeyLoadedData = { frag, keyInfo }; if (this.emeController && this.config.emeEnabled) { + if (!keyInfo.decryptdata.keyId && frag.initSegment?.data) { + const keyIds = parseKeyIdsFromTenc( + frag.initSegment.data as Uint8Array, + ); + if (keyIds.length) { + const keyId = keyIds[0]; + if (keyId.some((b) => b !== 0)) { + this.log(`Using keyId found in init segment ${arrayToHex(keyId)}`); + keyInfo.decryptdata.keyId = keyId; + LevelKey.setKeyIdForUri(keyInfo.decryptdata.uri, keyId); + } + } + } const keySessionContextPromise = this.emeController.loadKey(keyLoadedData); return (keyInfo.keyLoadPromise = keySessionContextPromise.then( diff --git a/src/loader/level-key.ts b/src/loader/level-key.ts index 2c6a8d076cb..31a7c077d3d 100644 --- a/src/loader/level-key.ts +++ b/src/loader/level-key.ts @@ -100,7 +100,10 @@ export class LevelKey implements DecryptData { return false; } - public getDecryptData(sn: number | 'initSegment'): LevelKey | null { + public getDecryptData( + sn: number | 'initSegment', + levelKeys?: { [key: string]: LevelKey | undefined }, + ): LevelKey | null { if (!this.encrypted || !this.uri) { return null; } @@ -135,10 +138,19 @@ export class LevelKey implements DecryptData { return this; } - if (this.pssh && this.keyId) { - return this; + if (this.keyId) { + // Handle case where key id is changed in KEY_LOADING event handler #7542#issuecomment-3305203929 + const assignedKeyId = keyUriToKeyIdMap[this.uri]; + if (assignedKeyId && !arrayValuesMatch(this.keyId, assignedKeyId)) { + LevelKey.setKeyIdForUri(this.uri, this.keyId); + } + + if (this.pssh) { + return this; + } } + // Key bytes are signalled the KEYID attribute, typically only found on WideVine KEY tags // Initialize keyId if possible const keyBytes = convertDataUriToArrayBytes(this.uri); if (keyBytes) { @@ -156,8 +168,7 @@ export class LevelKey implements DecryptData { } } if (!this.keyId) { - const offset = keyBytes.length - 22; - this.keyId = keyBytes.subarray(offset, offset + 16); + this.keyId = getKeyIdFromPlayReadyKey(levelKeys); } break; case KeySystemFormats.PLAYREADY: { @@ -187,24 +198,49 @@ export class LevelKey implements DecryptData { } } - // Default behavior: assign a new keyId for each uri + // Default behavior: get keyId from other KEY tag or URI lookup if (!this.keyId || this.keyId.byteLength !== 16) { - let keyId = keyUriToKeyIdMap[this.uri]; + let keyId: Uint8Array | null | undefined; + keyId = getKeyIdFromWidevineKey(levelKeys); if (!keyId) { - const val = - Object.keys(keyUriToKeyIdMap).length % Number.MAX_SAFE_INTEGER; - keyId = new Uint8Array(16); - const dv = new DataView(keyId.buffer, 12, 4); // Just set the last 4 bytes - dv.setUint32(0, val); + keyId = getKeyIdFromPlayReadyKey(levelKeys); + if (!keyId) { + keyId = keyUriToKeyIdMap[this.uri]; + } + } + if (keyId) { + this.keyId = keyId; LevelKey.setKeyIdForUri(this.uri, keyId); } - this.keyId = keyId; } return this; } } +function getKeyIdFromWidevineKey( + levelKeys: { [key: string]: LevelKey | undefined } | undefined, +) { + const widevineKey = levelKeys?.[KeySystemFormats.WIDEVINE]; + if (widevineKey) { + return widevineKey.keyId; + } + return null; +} + +function getKeyIdFromPlayReadyKey( + levelKeys: { [key: string]: LevelKey | undefined } | undefined, +) { + const playReadyKey = levelKeys?.[KeySystemFormats.PLAYREADY]; + if (playReadyKey) { + const playReadyKeyBytes = convertDataUriToArrayBytes(playReadyKey.uri); + if (playReadyKeyBytes) { + return parsePlayReadyWRM(playReadyKeyBytes); + } + } + return null; +} + function createInitializationVector(segmentNumber: number) { const uint8View = new Uint8Array(16); for (let i = 12; i < 16; i++) { diff --git a/src/utils/mp4-tools.ts b/src/utils/mp4-tools.ts index be093141986..4d6d836db2f 100644 --- a/src/utils/mp4-tools.ts +++ b/src/utils/mp4-tools.ts @@ -7,6 +7,8 @@ import type { DecryptData } from '../loader/level-key'; import type { PassthroughTrack, UserdataSample } from '../types/demuxer'; import type { ILogger } from '../utils/logger'; +type BoxDataOrUndefined = Uint8Array | undefined; + const UINT32_MAX = Math.pow(2, 32) - 1; const push = [].push; @@ -573,51 +575,74 @@ export function patchEncyptionData( } const keyId = decryptdata.keyId; if (keyId && decryptdata.isCommonEncryption) { - const traks = findBox(initSegment, ['moov', 'trak']); - traks.forEach((trak) => { - const stsd = findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0]; - - // skip the sample entry count - const sampleEntries = stsd.subarray(8); - let encBoxes = findBox(sampleEntries, ['enca']); - const isAudio = encBoxes.length > 0; - if (!isAudio) { - encBoxes = findBox(sampleEntries, ['encv']); + applyToTencBoxes(initSegment, (tenc, isAudio) => { + // Look for default key id (keyID offset is always 8 within the tenc box): + const tencKeyId = tenc.subarray(8, 24); + if (!tencKeyId.some((b) => b !== 0)) { + logger.log( + `[eme] Patching keyId in 'enc${ + isAudio ? 'a' : 'v' + }>sinf>>tenc' box: ${arrayToHex(tencKeyId)} -> ${arrayToHex(keyId)}`, + ); + tenc.set(keyId, 8); } - encBoxes.forEach((enc) => { - const encBoxChildren = isAudio ? enc.subarray(28) : enc.subarray(78); - const sinfBoxes = findBox(encBoxChildren, ['sinf']); - sinfBoxes.forEach((sinf) => { - const tenc = parseSinf(sinf); - if (tenc) { - // Look for default key id (keyID offset is always 8 within the tenc box): - const tencKeyId = tenc.subarray(8, 24) as Uint8Array; - if (!tencKeyId.some((b) => b !== 0)) { - logger.log( - `[eme] Patching keyId in 'enc${ - isAudio ? 'a' : 'v' - }>sinf>>tenc' box: ${arrayToHex(tencKeyId)} -> ${arrayToHex( - keyId, - )}`, - ); - tenc.set(keyId, 8); - } - } - }); - }); }); } } -export function parseSinf(sinf: Uint8Array): Uint8Array | null { - const schm = findBox(sinf, ['schm'])[0]; - if (schm as any) { +export function parseKeyIdsFromTenc( + initSegment: Uint8Array, +): Uint8Array[] { + const keyIds: Uint8Array[] = []; + applyToTencBoxes(initSegment, (tenc) => keyIds.push(tenc.subarray(8, 24))); + return keyIds; +} + +function applyToTencBoxes( + initSegment: Uint8Array, + predicate: (tenc: Uint8Array, isAudio: boolean) => void, +) { + const traks = findBox(initSegment, ['moov', 'trak']); + traks.forEach((trak) => { + const stsd = findBox(trak, [ + 'mdia', + 'minf', + 'stbl', + 'stsd', + ])[0] as BoxDataOrUndefined; + if (!stsd) return; + const sampleEntries = stsd.subarray(8); + let encBoxes = findBox(sampleEntries, ['enca']); + const isAudio = encBoxes.length > 0; + if (!isAudio) { + encBoxes = findBox(sampleEntries, ['encv']); + } + encBoxes.forEach((enc) => { + const encBoxChildren = isAudio ? enc.subarray(28) : enc.subarray(78); + const sinfBoxes = findBox(encBoxChildren, ['sinf']); + sinfBoxes.forEach((sinf) => { + const tenc = parseSinf(sinf); + if (tenc) { + predicate(tenc, isAudio); + } + }); + }); + }); +} + +export function parseSinf(sinf: Uint8Array): BoxDataOrUndefined { + const schm = findBox(sinf, ['schm'])[0] as BoxDataOrUndefined; + if (schm) { const scheme = bin2str(schm.subarray(4, 8)); if (scheme === 'cbcs' || scheme === 'cenc') { - return findBox(sinf, ['schi', 'tenc'])[0]; + const tenc = findBox(sinf, ['schi', 'tenc'])[0] as BoxDataOrUndefined; + if (tenc) { + return tenc; + } + } else if (scheme === 'cbc2') { + /* no-op */ } } - return null; } /* diff --git a/tests/functional/auto/style.css b/tests/functional/auto/style.css index 299ceb1096d..a3ef1755261 100644 --- a/tests/functional/auto/style.css +++ b/tests/functional/auto/style.css @@ -2,6 +2,10 @@ body { background-color: #cccccc; } +#video { + width: 100%; +} + #log { position: fixed; top: 0; diff --git a/tests/functional/auto/testbench.js b/tests/functional/auto/testbench.js index 7cc540afc2b..98190d7b821 100644 --- a/tests/functional/auto/testbench.js +++ b/tests/functional/auto/testbench.js @@ -109,23 +109,42 @@ function startStream(streamUrl, config, callback, autoplay) { ); console.log('[test] > userAgent:', navigator.userAgent); if (autoplay !== false) { - hls.on(Hls.Events.MANIFEST_PARSED, function () { - console.log('[test] > Manifest parsed. Calling video.play()'); + // attempt to ready playback in case test start is treated as a user interaction + video.src = null; + video.load(); + + hls.on(Hls.Events.MEDIA_ATTACHED, function () { + console.log('[test] > Media attached. Calling video.play()'); var playPromise = video.play(); if (playPromise) { - playPromise.catch(function (error) { - console.log( - '[test] > video.play() failed with error: ' + - error.name + - ' ' + - error.message - ); - if (error.name === 'NotAllowedError') { - console.log('[test] > Attempting to play with video muted'); - video.muted = true; - return video.play(); - } - }); + playPromise + .catch(function (error) { + if (error.name === 'NotAllowedError') { + console.log('[test] > Attempting to play with video muted'); + video.muted = true; + return video.play(); + } + throw error; + }) + .then(function () { + video.controls = true; + console.log( + '[test] > video.play() resolved' + + (video.muted ? ' (muted)' : '') + + ' currentTime: ' + + video.currentTime + ); + }) + .catch(function (error) { + console.log( + '[test] > video.play()' + + (video.muted ? ' (muted)' : '') + + ' failed with error: ' + + error.name + + ' ' + + error.message + ); + }); } }); } diff --git a/tests/unit/controller/eme-controller.ts b/tests/unit/controller/eme-controller.ts index 2acf1c2db14..452bc664464 100644 --- a/tests/unit/controller/eme-controller.ts +++ b/tests/unit/controller/eme-controller.ts @@ -23,8 +23,11 @@ type EMEControllerTestable = Omit< > & { hls: HlsMock; mediaKeySessions: MediaKeySessionContext[]; - keyIdToKeySessionPromise: { - [keyId: string]: Promise | undefined; + keyUriToSessionPromise: { + [keyUri: string]: Promise | undefined; + }; + keyUriToLevelKeys: { + [keyUri: string]: LevelKey[] | undefined; }; onMediaAttached: ( event: Events.MEDIA_ATTACHED, @@ -367,9 +370,9 @@ describe('EMEController', function () { type: 'main', } as any) .then(() => { - expect(emeController.keyIdToKeySessionPromise).to.deep.equal( + expect(emeController.keyUriToSessionPromise).to.deep.equal( {}, - '`keyIdToKeySessionPromise` should be an empty dictionary when no key IDs are found', + '`keyUriToSessionPromise` should be an empty dictionary when no key IDs are found', ); }); }); @@ -395,10 +398,11 @@ describe('EMEController', function () { const levelKey = getParsedLevelKey(); const keySession = new MediaKeySessionMock2(); const mockMediaKeySessionContext: MediaKeySessionContext = { - decryptdata: levelKey, keySystem: KeySystems.FAIRPLAY, + levelKeys: [levelKey], mediaKeys: new MediaKeysMock(), mediaKeysSession: keySession, + keyStatuses: {}, }; const keyStatuses = emeController.getKeyStatuses( @@ -462,19 +466,17 @@ describe('EMEController', function () { }, } as any) .then(() => { - expect( - emeController.keyIdToKeySessionPromise[ - '00000000000000000000000000000000' - ], - ).to.be.a('Promise'); - return emeController.keyIdToKeySessionPromise[ - '00000000000000000000000000000000' - ]!.finally(() => { - expect(mediaKeysSetServerCertificateSpy).to.have.been.calledOnce; - expect(mediaKeysSetServerCertificateSpy).to.have.been.calledWith( - sinon.match({ byteLength: 6 }), - ); - }); + expect(emeController.keyUriToSessionPromise['data://key-uri']).to.be.a( + 'Promise', + ); + return emeController.keyUriToSessionPromise['data://key-uri']!.finally( + () => { + expect(mediaKeysSetServerCertificateSpy).to.have.been.calledOnce; + expect(mediaKeysSetServerCertificateSpy).to.have.been.calledWith( + sinon.match({ byteLength: 6 }), + ); + }, + ); }); }); @@ -543,14 +545,12 @@ describe('EMEController', function () { // expected? }); - expect( - emeController.keyIdToKeySessionPromise[ - '00000000000000000000000000000000' - ], - ).to.be.a('Promise'); - return emeController.keyIdToKeySessionPromise[ - '00000000000000000000000000000000' - ]!.catch(() => {}).finally(() => { + expect(emeController.keyUriToSessionPromise['data://key-uri']).to.be.a( + 'Promise', + ); + return emeController.keyUriToSessionPromise['data://key-uri']!.catch( + () => {}, + ).finally(() => { expect(mediaKeysSetServerCertificateSpy).to.have.been.calledOnce; expect((mediaKeysSetServerCertificateSpy.args[0] as any)[0]).to.equal( xhrInstance.response, @@ -621,14 +621,12 @@ describe('EMEController', function () { // expected? }); - expect( - emeController.keyIdToKeySessionPromise[ - '00000000000000000000000000000000' - ], - ).to.be.a('Promise'); - return emeController.keyIdToKeySessionPromise[ - '00000000000000000000000000000000' - ]!.catch(() => {}).finally(() => { + expect(emeController.keyUriToSessionPromise['data://key-uri']).to.be.a( + 'Promise', + ); + return emeController.keyUriToSessionPromise['data://key-uri']!.catch( + () => {}, + ).finally(() => { expect(emeController.hls.trigger).to.have.been.calledOnce; expect(emeController.hls.trigger.args[0][1].details).to.equal( ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED, @@ -669,10 +667,11 @@ describe('EMEController', function () { const levelKey = getParsedLevelKey(); const keySession = new MediaKeySessionMock(); const mockMediaKeySessionContext: MediaKeySessionContext = { - decryptdata: levelKey, keySystem: KeySystems.FAIRPLAY, + levelKeys: [levelKey], mediaKeys: new MediaKeysMock(), mediaKeysSession: keySession, + keyStatuses: {}, }; sinon.stub(keySession, 'remove'); sinon.stub(keySession, 'close'); @@ -716,10 +715,11 @@ describe('EMEController', function () { const levelKey = getParsedLevelKey(); const keySession = new MediaKeySessionMock(); const mockMediaKeySessionContext: MediaKeySessionContext = { - decryptdata: levelKey, keySystem: KeySystems.FAIRPLAY, + levelKeys: [levelKey], mediaKeys: new MediaKeysMock(), mediaKeysSession: keySession, + keyStatuses: {}, }; sinon.stub(keySession, 'remove'); const keySessionCloseSpy = sinon.stub(keySession, 'close'); @@ -774,10 +774,11 @@ describe('EMEController', function () { const levelKey = getParsedLevelKey(); const keySession = new MediaKeySessionMock(); const mockMediaKeySessionContext: MediaKeySessionContext = { - decryptdata: levelKey, keySystem: KeySystems.FAIRPLAY, + levelKeys: [levelKey], mediaKeys: new MediaKeysMock(), mediaKeysSession: keySession, + keyStatuses: {}, }; sinon.stub(keySession, 'remove'); const keySessionCloseSpy = sinon.stub(keySession, 'close');