Skip to content

Commit

Permalink
Parse multiple pssh from "encrypted" init data and fix Widevine key I… (
Browse files Browse the repository at this point in the history
#6640)

* Parse multiple pssh from "encrypted" init data and fix Widevine key ID extraction for playlist match
Fixes #6636

* Logging and assertion improvements per @cjpillsbury's review

* Logging and assertion improvements per @cjpillsbury's review

(cherry picked from commit a960251)
  • Loading branch information
robwalch committed Aug 22, 2024
1 parent 0d29c78 commit 6a190fc
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 66 deletions.
58 changes: 39 additions & 19 deletions src/controller/eme-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,20 @@ import {
keySystemFormatToKeySystemDomain,
KeySystemIds,
keySystemIdToKeySystemDomain,
} from '../utils/mediakeys-helper';
import {
KeySystems,
requestMediaKeySystemAccess,
} from '../utils/mediakeys-helper';
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';
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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);
Expand All @@ -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;
Expand Down
19 changes: 11 additions & 8 deletions src/utils/mediakeys-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}

Expand All @@ -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;
}
}

Expand Down
99 changes: 72 additions & 27 deletions src/utils/mp4-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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,
};
}
16 changes: 4 additions & 12 deletions tests/unit/controller/eme-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down

0 comments on commit 6a190fc

Please sign in to comment.