diff --git a/src/controller/eme-controller.ts b/src/controller/eme-controller.ts index ae2c40f249e..cbeef66f205 100644 --- a/src/controller/eme-controller.ts +++ b/src/controller/eme-controller.ts @@ -14,8 +14,6 @@ import { keySystemFormatToKeySystemDomain, KeySystemIds, keySystemIdToKeySystemDomain, -} from '../utils/mediakeys-helper'; -import { KeySystems, requestMediaKeySystemAccess, } from '../utils/mediakeys-helper'; @@ -23,7 +21,13 @@ import { strToUtf8array } from '../utils/keysystem-util'; import { base64Decode } from '../utils/numeric-encoding-utils'; import { DecryptData, LevelKey } from '../loader/level-key'; import Hex from '../utils/hex'; -import { bin2str, parsePssh, parseSinf } from '../utils/mp4-tools'; +import { + bin2str, + parseMultiPssh, + parseSinf, + PsshData, + PsshInvalidResult, +} from '../utils/mp4-tools'; import { EventEmitter } from 'eventemitter3'; import type Hls from '../hls'; import type { ComponentAPI } from '../types/component-api'; @@ -525,7 +529,8 @@ class EMEController implements ComponentAPI { private _onMediaEncrypted(event: MediaEncryptedEvent) { const { initDataType, initData } = event; - this.debug(`"${event.type}" event: init data type: "${initDataType}"`); + const logMessage = `"${event.type}" event: init data type: "${initDataType}"`; + this.debug(logMessage); // Ignore event when initData is null if (initData === null) { @@ -545,30 +550,42 @@ class EMEController implements ComponentAPI { const sinf = base64Decode(JSON.parse(json).sinf); const tenc = parseSinf(new Uint8Array(sinf)); if (!tenc) { - return; + 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('Failed to parse sinf "encrypted" event message initData'); + this.warn(`${logMessage} Failed to parse sinf: ${error}`); return; } } else { - // Support clear-lead key-session creation (otherwise depend on playlist keys) - const psshInfo = parsePssh(initData); - if (psshInfo === null) { + // Support Widevine clear-lead key-session creation (otherwise depend on playlist keys) + const psshResults = parseMultiPssh(initData); + const psshInfo = psshResults.filter( + (pssh): pssh is PsshData => pssh.systemId === KeySystemIds.WIDEVINE, + )[0]; + 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; } - if ( - psshInfo.version === 0 && - psshInfo.systemId === KeySystemIds.WIDEVINE && - psshInfo.data - ) { - keyId = psshInfo.data.subarray(8, 24); + keySystemDomain = keySystemIdToKeySystemDomain(psshInfo.systemId); + if (psshInfo.version === 0 && psshInfo.data) { + const offset = psshInfo.data.length - 22; + keyId = psshInfo.data.subarray(offset, offset + 16); } - keySystemDomain = keySystemIdToKeySystemDomain( - psshInfo.systemId as KeySystemIds, - ); } if (!keySystemDomain || !keyId) { @@ -583,7 +600,7 @@ class EMEController implements ComponentAPI { // Match playlist key const keyContext = mediaKeySessions[i]; const decryptdata = keyContext.decryptdata; - if (decryptdata.pssh || !decryptdata.keyId) { + if (!decryptdata.keyId) { continue; } const oldKeyIdHex = Hex.hexDump(decryptdata.keyId); @@ -592,6 +609,9 @@ class EMEController implements ComponentAPI { 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; diff --git a/src/utils/mediakeys-helper.ts b/src/utils/mediakeys-helper.ts index 029813f3a4a..426fb43a026 100755 --- a/src/utils/mediakeys-helper.ts +++ b/src/utils/mediakeys-helper.ts @@ -36,10 +36,10 @@ export function keySystemFormatToKeySystemDomain( // System IDs for which we can extract a key ID from "encrypted" event PSSH export const enum KeySystemIds { - // CENC = '1077efecc0b24d02ace33c1e52e2fb4b' - // CLEARKEY = 'e2719d58a985b3c9781ab030af78d30e', - // FAIRPLAY = '94ce86fb07ff4f43adb893d2fa968ca2', - // PLAYREADY = '9a04f07998404286ab92e65be0885f95', + CENC = '1077efecc0b24d02ace33c1e52e2fb4b', + CLEARKEY = 'e2719d58a985b3c9781ab030af78d30e', + FAIRPLAY = '94ce86fb07ff4f43adb893d2fa968ca2', + PLAYREADY = '9a04f07998404286ab92e65be0885f95', WIDEVINE = 'edef8ba979d64acea3c827dcd51d21ed', } @@ -48,10 +48,13 @@ export function keySystemIdToKeySystemDomain( ): KeySystems | undefined { if (systemId === KeySystemIds.WIDEVINE) { return KeySystems.WIDEVINE; - // } else if (systemId === KeySystemIds.PLAYREADY) { - // return KeySystems.PLAYREADY; - // } else if (systemId === KeySystemIds.CENC || systemId === KeySystemIds.CLEARKEY) { - // return KeySystems.CLEARKEY; + } else if (systemId === KeySystemIds.PLAYREADY) { + return KeySystems.PLAYREADY; + } else if ( + systemId === KeySystemIds.CENC || + systemId === KeySystemIds.CLEARKEY + ) { + return KeySystems.CLEARKEY; } } diff --git a/src/utils/mp4-tools.ts b/src/utils/mp4-tools.ts index f06ba208956..a9908b28ee5 100644 --- a/src/utils/mp4-tools.ts +++ b/src/utils/mp4-tools.ts @@ -3,6 +3,7 @@ import { sliceUint8 } from './typed-array'; import { utf8ArrayToStr } from '../demux/id3'; import { logger } from '../utils/logger'; import Hex from './hex'; +import type { KeySystemIds } from './mediakeys-helper'; import type { PassthroughTrack, UserdataSample } from '../types/demuxer'; import type { DecryptData } from '../loader/level-key'; @@ -545,7 +546,6 @@ export function parseSinf(sinf: Uint8Array): Uint8Array | null { return findBox(sinf, ['schi', 'tenc'])[0]; } } - logger.error(`[eme] missing 'schm' box`); return null; } @@ -1349,41 +1349,86 @@ export function mp4pssh( ); } -export function parsePssh(initData: ArrayBuffer) { - if (!(initData instanceof ArrayBuffer) || initData.byteLength < 32) { - return null; +export type PsshData = { + version: 0 | 1; + systemId: KeySystemIds; + kids: null | Uint8Array[]; + data: null | Uint8Array; + offset: number; + size: number; +}; + +export type PsshInvalidResult = { + systemId?: undefined; + offset: number; + size: number; +}; + +export function parseMultiPssh( + initData: ArrayBuffer, +): (PsshData | PsshInvalidResult)[] { + const results: (PsshData | PsshInvalidResult)[] = []; + if (initData instanceof ArrayBuffer) { + const length = initData.byteLength; + let offset = 0; + while (offset + 32 < length) { + const view = new DataView(initData, offset); + const pssh = parsePssh(view); + results.push(pssh); + offset += pssh.size; + } } - const result = { - version: 0, - systemId: '', - kids: null as null | Uint8Array[], - data: null as null | Uint8Array, - }; - const view = new DataView(initData); - const boxSize = view.getUint32(0); - if (initData.byteLength !== boxSize && boxSize > 44) { - return null; + return results; +} + +function parsePssh(view: DataView): PsshData | PsshInvalidResult { + const size = view.getUint32(0); + const offset = view.byteOffset; + const length = view.byteLength; + if (length < size) { + return { + offset, + size: length, + }; } const type = view.getUint32(4); if (type !== 0x70737368) { - return null; + return { offset, size }; } - result.version = view.getUint32(8) >>> 24; - if (result.version > 1) { - return null; + const version = view.getUint32(8) >>> 24; + if (version !== 0 && version !== 1) { + return { offset, size }; } - result.systemId = Hex.hexDump(new Uint8Array(initData, 12, 16)); + const buffer = view.buffer; + const systemId = Hex.hexDump( + new Uint8Array(buffer, offset + 12, 16), + ) as KeySystemIds; const dataSizeOrKidCount = view.getUint32(28); - if (result.version === 0) { - if (boxSize - 32 < dataSizeOrKidCount) { - return null; + let kids: null | Uint8Array[] = null; + let data: null | Uint8Array = null; + if (version === 0) { + if (size - 32 < dataSizeOrKidCount || dataSizeOrKidCount < 22) { + return { offset, size }; + } + data = new Uint8Array(buffer, offset + 32, dataSizeOrKidCount); + } else if (version === 1) { + if ( + !dataSizeOrKidCount || + length < offset + 32 + dataSizeOrKidCount * 16 + 16 + ) { + return { offset, size }; } - result.data = new Uint8Array(initData, 32, dataSizeOrKidCount); - } else if (result.version === 1) { - result.kids = []; + kids = []; for (let i = 0; i < dataSizeOrKidCount; i++) { - result.kids.push(new Uint8Array(initData, 32 + i * 16, 16)); + kids.push(new Uint8Array(buffer, offset + 32 + i * 16, 16)); } } - return result; + return { + version, + systemId, + kids, + data, + offset, + size, + }; } diff --git a/tests/unit/controller/eme-controller.ts b/tests/unit/controller/eme-controller.ts index b67b734cf8f..0556f9af5c3 100644 --- a/tests/unit/controller/eme-controller.ts +++ b/tests/unit/controller/eme-controller.ts @@ -295,18 +295,10 @@ describe('EMEController', function () { media.emit('encrypted', badData); - expect(emeController.keyIdToKeySessionPromise.f000ba00).to.be.a('Promise'); - if (!emeController.keyIdToKeySessionPromise.f000ba00) { - return; - } - return emeController.keyIdToKeySessionPromise.f000ba00 - .catch(() => {}) - .finally(() => { - expect(emeController.hls.trigger).callCount(1); - expect(emeController.hls.trigger.args[0][1].details).to.equal( - ErrorDetails.KEY_SYSTEM_NO_SESSION, - ); - }); + 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 () {