Skip to content

Commit

Permalink
Support "clear-lead" key-session creation without new config
Browse files Browse the repository at this point in the history
  • Loading branch information
robwalch committed Oct 27, 2022
1 parent 264fd05 commit d145503
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 60 deletions.
27 changes: 13 additions & 14 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,6 @@ export type EMEControllerConfig = {
licenseXhrSetup?: (this: Hls, xhr: XMLHttpRequest, url: string, keyContext: MediaKeySessionContext, licenseChallenge: Uint8Array) => void | Promise<Uint8Array | void>;
licenseResponseCallback?: (this: Hls, xhr: XMLHttpRequest, url: string, keyContext: MediaKeySessionContext) => ArrayBuffer;
emeEnabled: boolean;
useEmeEncryptedEvent: boolean;
widevineLicenseUrl?: string;
drmSystems: DRMSystemsConfiguration;
drmSystemOptions: DRMSystemOptions;
Expand Down Expand Up @@ -2281,19 +2280,19 @@ export interface UserdataSample {
// Warnings were encountered during analysis:
//
// src/config.ts:79:3 - (ae-forgotten-export) The symbol "MediaKeySessionContext" needs to be exported by the entry point hls.d.ts
// src/config.ts:95:3 - (ae-forgotten-export) The symbol "DRMSystemsConfiguration" needs to be exported by the entry point hls.d.ts
// src/config.ts:198:3 - (ae-forgotten-export) The symbol "ILogger" needs to be exported by the entry point hls.d.ts
// src/config.ts:208:3 - (ae-forgotten-export) The symbol "AudioStreamController" needs to be exported by the entry point hls.d.ts
// src/config.ts:209:3 - (ae-forgotten-export) The symbol "AudioTrackController" needs to be exported by the entry point hls.d.ts
// src/config.ts:211:3 - (ae-forgotten-export) The symbol "SubtitleStreamController" needs to be exported by the entry point hls.d.ts
// src/config.ts:212:3 - (ae-forgotten-export) The symbol "SubtitleTrackController" needs to be exported by the entry point hls.d.ts
// src/config.ts:213:3 - (ae-forgotten-export) The symbol "TimelineController" needs to be exported by the entry point hls.d.ts
// src/config.ts:215:3 - (ae-forgotten-export) The symbol "EMEController" needs to be exported by the entry point hls.d.ts
// src/config.ts:218:3 - (ae-forgotten-export) The symbol "CMCDController" needs to be exported by the entry point hls.d.ts
// src/config.ts:220:3 - (ae-forgotten-export) The symbol "AbrController" needs to be exported by the entry point hls.d.ts
// src/config.ts:221:3 - (ae-forgotten-export) The symbol "BufferController" needs to be exported by the entry point hls.d.ts
// src/config.ts:222:3 - (ae-forgotten-export) The symbol "CapLevelController" needs to be exported by the entry point hls.d.ts
// src/config.ts:223:3 - (ae-forgotten-export) The symbol "FPSController" needs to be exported by the entry point hls.d.ts
// src/config.ts:94:3 - (ae-forgotten-export) The symbol "DRMSystemsConfiguration" needs to be exported by the entry point hls.d.ts
// src/config.ts:197:3 - (ae-forgotten-export) The symbol "ILogger" needs to be exported by the entry point hls.d.ts
// src/config.ts:207:3 - (ae-forgotten-export) The symbol "AudioStreamController" needs to be exported by the entry point hls.d.ts
// src/config.ts:208:3 - (ae-forgotten-export) The symbol "AudioTrackController" needs to be exported by the entry point hls.d.ts
// src/config.ts:210:3 - (ae-forgotten-export) The symbol "SubtitleStreamController" needs to be exported by the entry point hls.d.ts
// src/config.ts:211:3 - (ae-forgotten-export) The symbol "SubtitleTrackController" needs to be exported by the entry point hls.d.ts
// src/config.ts:212:3 - (ae-forgotten-export) The symbol "TimelineController" needs to be exported by the entry point hls.d.ts
// src/config.ts:214:3 - (ae-forgotten-export) The symbol "EMEController" needs to be exported by the entry point hls.d.ts
// src/config.ts:217:3 - (ae-forgotten-export) The symbol "CMCDController" needs to be exported by the entry point hls.d.ts
// src/config.ts:219:3 - (ae-forgotten-export) The symbol "AbrController" needs to be exported by the entry point hls.d.ts
// src/config.ts:220:3 - (ae-forgotten-export) The symbol "BufferController" needs to be exported by the entry point hls.d.ts
// src/config.ts:221:3 - (ae-forgotten-export) The symbol "CapLevelController" needs to be exported by the entry point hls.d.ts
// src/config.ts:222:3 - (ae-forgotten-export) The symbol "FPSController" needs to be exported by the entry point hls.d.ts

// (No @packageDocumentation comment for this package)

Expand Down
2 changes: 0 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ export type EMEControllerConfig = {
keyContext: MediaKeySessionContext
) => ArrayBuffer;
emeEnabled: boolean;
useEmeEncryptedEvent: boolean;
widevineLicenseUrl?: string;
drmSystems: DRMSystemsConfiguration;
drmSystemOptions: DRMSystemOptions;
Expand Down Expand Up @@ -311,7 +310,6 @@ export const hlsDefaultConfig: HlsConfig = {
maxLoadingDelay: 4, // used by abr-controller
minAutoBitrate: 0, // used by hls
emeEnabled: false, // used by eme-controller
useEmeEncryptedEvent: false, // used by eme-controller
widevineLicenseUrl: undefined, // used by eme-controller
drmSystems: {}, // used by eme-controller
drmSystemOptions: {}, // used by eme-controller
Expand Down
90 changes: 54 additions & 36 deletions src/controller/eme-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
keySystemDomainToKeySystemFormat as keySystemToKeySystemFormat,
KeySystemFormats,
keySystemFormatToKeySystemDomain,
KeySystemIds,
keySystemIdToKeySystemDomain,
} from '../utils/mediakeys-helper';
import {
KeySystems,
Expand All @@ -31,6 +33,7 @@ import type {
import type { EMEControllerConfig } from '../config';
import type { Fragment } from '../loader/fragment';
import Hex from '../utils/hex';
import { parsePssh } from '../utils/mp4-tools';

const MAX_LICENSE_REQUEST_FAILURES = 3;
const LOGGER_PREFIX = '[eme]';
Expand Down Expand Up @@ -410,10 +413,6 @@ class EMEController implements ComponentAPI {

this.log(`Starting session for key ${keyDetails}`);

if (this.media && !this.config.useEmeEncryptedEvent) {
this.media.removeEventListener('encrypted', this.onMediaEncrypted);
}

let keySessionContextPromise = this.keyIdToKeySessionPromise[keyId];
if (!keySessionContextPromise) {
keySessionContextPromise = this.keyIdToKeySessionPromise[keyId] =
Expand All @@ -431,10 +430,6 @@ class EMEController implements ComponentAPI {
mediaKeys,
decryptdata,
});
if (this.config.useEmeEncryptedEvent) {
// Use 'encrypted' event initData and type rather than 'cenc' pssh from level-key
return keySessionContext;
}
return this.generateRequestWithPreferredKeySession(
keySessionContext,
'cenc',
Expand Down Expand Up @@ -500,38 +495,63 @@ class EMEController implements ComponentAPI {
}

private _onMediaEncrypted(event: MediaEncryptedEvent) {
this.log(`"${event.type}" event: init data type: "${event.initDataType}"`);
const { initDataType, initData } = event;
this.log(`"${event.type}" event: init data type: "${initDataType}"`);

if (!this.config.useEmeEncryptedEvent) {
// Ignore event when initData is null
if (initData === null) {
return;
}

let keySessionContextPromise = this.keyIdToKeySessionPromise.encrypted;
// Support clear-lead key-session creation (otherwise depend on playlist keys)
const psshInfo = parsePssh(initData);
if (psshInfo === null) {
return;
}
let keyId: Uint8Array | null = null;
if (
psshInfo.version === 0 &&
psshInfo.systemId === KeySystemIds.WIDEVINE &&
psshInfo.data
) {
keyId = psshInfo.data.subarray(8, 24);
}
const keySystemDomain = keySystemIdToKeySystemDomain(
psshInfo.systemId as KeySystemIds
);
if (!keySystemDomain || !keyId) {
return;
}

const keyIdHex = Hex.hexDump(keyId);
let keySessionContextPromise = this.keyIdToKeySessionPromise[keyIdHex];
if (!keySessionContextPromise) {
keySessionContextPromise = this.keyIdToKeySessionPromise.encrypted =
this.getKeySystemSelectionPromise().then(({ keySystem, mediaKeys }) => {
this.throwIfDestroyed();
const sessionParameters = {
decryptdata: new LevelKey(
'UNKNOWN',
'encrypted',
keySystemToKeySystemFormat(keySystem) ?? ''
),
keySystem,
mediaKeys,
};
sessionParameters.decryptdata.keyId = new Uint8Array(16);
return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => {
keySessionContextPromise = this.keyIdToKeySessionPromise[keyIdHex] =
this.getKeySystemSelectionPromise([keySystemDomain]).then(
({ keySystem, mediaKeys }) => {
this.throwIfDestroyed();
const keySessionContext =
this.createMediaKeySessionContext(sessionParameters);
return this.generateRequestWithPreferredKeySession(
keySessionContext,
event.initDataType,
event.initData
const decryptdata = new LevelKey(
'ISO-23001-7',
keyIdHex,
keySystemToKeySystemFormat(keySystem) ?? ''
);
});
});
decryptdata.pssh = new Uint8Array(initData);
decryptdata.keyId = keyId;
return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => {
this.throwIfDestroyed();
const keySessionContext = this.createMediaKeySessionContext({
decryptdata,
keySystem,
mediaKeys,
});
return this.generateRequestWithPreferredKeySession(
keySessionContext,
initDataType,
initData
);
});
}
);
}
keySessionContextPromise.catch((error) => this.handleError(error));
}
Expand Down Expand Up @@ -1035,9 +1055,7 @@ class EMEController implements ComponentAPI {
// keep reference of media
this.media = media;

if (this.config.useEmeEncryptedEvent) {
media.addEventListener('encrypted', this.onMediaEncrypted);
}
media.addEventListener('encrypted', this.onMediaEncrypted);
media.addEventListener('waitingforkey', this.onWaitingForKey);
}

Expand Down
23 changes: 22 additions & 1 deletion src/utils/mediakeys-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export enum KeySystems {
WIDEVINE = 'com.widevine.alpha',
}

// Playlist parser
// Playlist #EXT-X-KEY KEYFORMAT values
export enum KeySystemFormats {
CLEARKEY = 'org.w3.clearkey',
FAIRPLAY = 'com.apple.streamingkeydelivery',
Expand All @@ -32,6 +32,27 @@ export function keySystemFormatToKeySystemDomain(
}
}

// System IDs for which we can extract a key ID from "encrypted" event PSSH
export enum KeySystemIds {
// CENC = '1077efecc0b24d02ace33c1e52e2fb4b'
// CLEARKEY = 'e2719d58a985b3c9781ab030af78d30e',
// FAIRPLAY = '94ce86fb07ff4f43adb893d2fa968ca2',
// PLAYREADY = '9a04f07998404286ab92e65be0885f95',
WIDEVINE = 'edef8ba979d64acea3c827dcd51d21ed',
}

export function keySystemIdToKeySystemDomain(
systemId: KeySystemIds
): 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;
}
}

export function keySystemDomainToKeySystemFormat(
keySystem: KeySystems
): KeySystemFormats | undefined {
Expand Down
39 changes: 39 additions & 0 deletions src/utils/mp4-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1092,3 +1092,42 @@ export function mp4pssh(
data || new Uint8Array()
);
}

export function parsePssh(initData: ArrayBuffer) {
if (!(initData instanceof ArrayBuffer) || initData.byteLength < 32) {
return null;
}
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;
}
const type = view.getUint32(4);
if (type !== 0x70737368) {
return null;
}
result.version = view.getUint32(8) >>> 24;
if (result.version > 1) {
return null;
}
result.systemId = Hex.hexDump(new Uint8Array(initData, 12, 16));
const dataSizeOrKidCount = view.getUint32(28);
if (result.version === 0) {
if (boxSize - 32 < dataSizeOrKidCount) {
return null;
}
result.data = new Uint8Array(initData, 32, dataSizeOrKidCount);
} else if (result.version === 1) {
result.kids = [];
for (let i = 0; i < dataSizeOrKidCount; i++) {
result.kids.push(new Uint8Array(initData, 32 + i * 16, 16));
}
}
return result;
}
26 changes: 19 additions & 7 deletions tests/unit/controller/eme-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ describe('EMEController', function () {
},
},
requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy,
// useEmeEncryptedEvent: true, // skip generate request, license exchange, and key status "usable"
});

sinonFakeXMLHttpRequestStatic.onCreate = (
Expand Down Expand Up @@ -249,7 +248,7 @@ describe('EMEController', function () {
});
});

it('should trigger key system error(s) when bad encrypted data is received', function () {
it('should ignore "encrypted" events with bad data', function () {
const reqMediaKsAccessSpy = sinon.spy(function () {
return Promise.resolve({
// Media-keys mock
Expand All @@ -271,13 +270,26 @@ describe('EMEController', function () {

setupEach({
emeEnabled: true,
useEmeEncryptedEvent: true,
requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy,
});

const badData = {
initDataType: 'cenc',
initData: 'bad data',
initData: new Uint8Array([
// box size
0, 0, 0, 44,
// "PSSH"
112, 115, 115, 104,
// version
0, 0, 0, 0,
// Widevine system id
237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33,
237,
// data size
0, 0, 0, 12,
// data (incomplete key)
0, 0, 0, 0, 0, 0, 0, 0, 240, 0, 186, 0,
]).buffer,
};

emeController.onMediaAttached(Events.MEDIA_ATTACHED, {
Expand All @@ -286,11 +298,11 @@ describe('EMEController', function () {

media.emit('encrypted', badData);

expect(emeController.keyIdToKeySessionPromise.encrypted).to.be.a('Promise');
if (!emeController.keyIdToKeySessionPromise.encrypted) {
expect(emeController.keyIdToKeySessionPromise.f000ba00).to.be.a('Promise');
if (!emeController.keyIdToKeySessionPromise.f000ba00) {
return;
}
return emeController.keyIdToKeySessionPromise.encrypted
return emeController.keyIdToKeySessionPromise.f000ba00
.catch(() => {})
.finally(() => {
expect(emeController.hls.trigger).callCount(1);
Expand Down

0 comments on commit d145503

Please sign in to comment.