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
…D extraction for playlist match

Fixes #6636
  • Loading branch information
robwalch committed Aug 21, 2024
1 parent 714a83f commit eb136b2
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 58 deletions.
29 changes: 17 additions & 12 deletions src/controller/eme-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,14 @@ import {
keySystemFormatToKeySystemDomain,
KeySystemIds,
keySystemIdToKeySystemDomain,
} from '../utils/mediakeys-helper';
import {
KeySystems,
requestMediaKeySystemAccess,
} from '../utils/mediakeys-helper';
import { strToUtf8array } from '../utils/utf8-utils';
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 } from '../utils/mp4-tools';
import { EventEmitter } from 'eventemitter3';
import type Hls from '../hls';
import type { ComponentAPI } from '../types/component-api';
Expand Down Expand Up @@ -542,20 +540,24 @@ class EMEController extends Logger implements ComponentAPI {
}
} else {
// Support clear-lead key-session creation (otherwise depend on playlist keys)
const psshInfo = parsePssh(initData);
if (psshInfo === null) {
const psshInfos = parseMultiPssh(initData);
const psshInfo = psshInfos.filter(
(pssh) => pssh.systemId === KeySystemIds.WIDEVINE,
)[0];
if (!psshInfo) {
return;
}
keySystemDomain = keySystemIdToKeySystemDomain(
psshInfo.systemId as KeySystemIds,
);
if (
psshInfo.version === 0 &&
psshInfo.systemId === KeySystemIds.WIDEVINE &&
psshInfo.data
psshInfo.data &&
psshInfo.data.length >= 30
) {
keyId = psshInfo.data.subarray(8, 24);
const offset = psshInfo.data.length - 22;
keyId = psshInfo.data.subarray(offset, offset + 16);
}
keySystemDomain = keySystemIdToKeySystemDomain(
psshInfo.systemId as KeySystemIds,
);
}

if (!keySystemDomain || !keyId) {
Expand All @@ -570,7 +572,7 @@ class EMEController extends Logger 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 @@ -579,6 +581,9 @@ class EMEController extends Logger 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
80 changes: 54 additions & 26 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 '@svta/common-media-library/utils/utf8ArrayToStr';
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 @@ -1351,41 +1352,68 @@ export function mp4pssh(
);
}

export function parsePssh(initData: ArrayBuffer) {
if (!(initData instanceof ArrayBuffer) || initData.byteLength < 32) {
return null;
type PsshData = {
version: 0 | 1;
systemId: KeySystemIds;
kids: null | Uint8Array[];
data: null | Uint8Array;
size: number;
};

export function parseMultiPssh(initData: ArrayBuffer): PsshData[] {
const results: PsshData[] = [];
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);
if ('systemId' in pssh) {
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 | { size: number } {
const size = view.getUint32(0);
const length = view.byteLength;
if (view.byteLength < size) {
return { size: length };
}
const type = view.getUint32(4);
if (type !== 0x70737368) {
return null;
return { 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 { size };
}
result.systemId = Hex.hexDump(new Uint8Array(initData, 12, 16));
const buffer = view.buffer;
const systemId = Hex.hexDump(
new Uint8Array(buffer, view.byteOffset + 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) {
return { size };
}
result.data = new Uint8Array(initData, 32, dataSizeOrKidCount);
} else if (result.version === 1) {
result.kids = [];
data = new Uint8Array(buffer, view.byteOffset + 32, dataSizeOrKidCount);
} else if (version === 1) {
kids = [];
for (let i = 0; i < dataSizeOrKidCount; i++) {
result.kids.push(new Uint8Array(initData, 32 + i * 16, 16));
kids.push(new Uint8Array(buffer, view.byteOffset + 32 + i * 16, 16));
}
}
return result;
return {
version,
systemId,
kids,
data,
size,
};
}
13 changes: 1 addition & 12 deletions tests/unit/controller/eme-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,18 +295,7 @@ 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({});
});

it('should fetch the server certificate and set it into the session', function () {
Expand Down

0 comments on commit eb136b2

Please sign in to comment.