Skip to content

Commit

Permalink
Only use selected key-system in EME "mediaencrypted" event handler
Browse files Browse the repository at this point in the history
Resolves #6947 / Suggested changes for #6946
  • Loading branch information
robwalch committed Jan 8, 2025
1 parent e346300 commit 98f7e26
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 125 deletions.
249 changes: 129 additions & 120 deletions src/controller/eme-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,147 +535,156 @@ class EMEController extends Logger implements ComponentAPI {
return;
}

let keyId: Uint8Array | null | undefined;
let keySystemDomain: KeySystems | undefined;

if (
initDataType === 'sinf' &&
this.getLicenseServerUrl(KeySystems.FAIRPLAY)
) {
// Match sinf keyId to playlist skd://keyId=
const json = bin2str(new Uint8Array(initData));
try {
const sinf = base64Decode(JSON.parse(json).sinf);
const tenc = parseSinf(sinf);
if (!tenc) {
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(`${logMessage} Failed to parse sinf: ${error}`);
return;
}
} else if (this.getLicenseServerUrl(KeySystems.WIDEVINE)) {
// Support Widevine clear-lead key-session creation (otherwise depend on playlist keys)
const psshResults = parseMultiPssh(initData);

// TODO: If using keySystemAccessPromises we might want to wait until one is resolved
if (!this.keyFormatPromise) {
let keySystems = Object.keys(
this.keySystemAccessPromises,
) as KeySystems[];
if (!keySystems.length) {
keySystems = getKeySystemsForConfig(this.config);
}
const keyFormats = keySystems
.map(keySystemToKeySystemFormat)
.filter((k) => !!k) as KeySystemFormats[];
this.keyFormatPromise = this.getKeyFormatPromise(keyFormats);
}

const psshInfo = psshResults.filter((pssh): pssh is PsshData => {
const keySystem = pssh.systemId
? keySystemIdToKeySystemDomain(pssh.systemId)
: null;
return keySystem ? keySystems.indexOf(keySystem) > -1 : false;
})[0];
this.keyFormatPromise.then((keySystemFormat) => {
const keySystem = keySystemFormatToKeySystemDomain(keySystemFormat);

let keyId: Uint8Array | null | undefined;
let keySystemDomain: KeySystems | undefined;

if (initDataType === 'sinf' && keySystem === KeySystems.FAIRPLAY) {
// Match sinf keyId to playlist skd://keyId=
const json = bin2str(new Uint8Array(initData));
try {
const sinf = base64Decode(JSON.parse(json).sinf);
const tenc = parseSinf(sinf);
if (!tenc) {
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(`${logMessage} Failed to parse sinf: ${error}`);
return;
}
} else {
// Support Widevine/PlayReady clear-lead key-session creation (otherwise depend on playlist keys)
const psshResults = parseMultiPssh(initData);

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`,
);
const psshInfo = psshResults.filter(
(pssh): pssh is PsshData =>
!!pssh.systemId &&
keySystemIdToKeySystemDomain(pssh.systemId) === keySystem,
)[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;
}
return;
}

keySystemDomain = keySystemIdToKeySystemDomain(psshInfo.systemId);
if (psshInfo.version === 0 && psshInfo.data) {
if (keySystemDomain === KeySystems.WIDEVINE) {
const offset = psshInfo.data.length - 22;
keyId = psshInfo.data.subarray(offset, offset + 16);
} else if (keySystemDomain === KeySystems.PLAYREADY) {
keyId = parsePlayReadyWRM(psshInfo.data);
keySystemDomain = keySystemIdToKeySystemDomain(psshInfo.systemId);
if (psshInfo.version === 0 && psshInfo.data) {
if (keySystemDomain === KeySystems.WIDEVINE) {
const offset = psshInfo.data.length - 22;
keyId = psshInfo.data.subarray(offset, offset + 16);
} else if (keySystemDomain === KeySystems.PLAYREADY) {
keyId = parsePlayReadyWRM(psshInfo.data);
}
}
}
}

if (!keySystemDomain || !keyId) {
return;
}
if (!keySystemDomain || !keyId) {
return;
}

const keyIdHex = Hex.hexDump(keyId);
const { keyIdToKeySessionPromise, mediaKeySessions } = this;
const keyIdHex = Hex.hexDump(keyId);
const { keyIdToKeySessionPromise, mediaKeySessions } = this;

let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex];
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 = Hex.hexDump(decryptdata.keyId);
if (
keyIdHex === oldKeyIdHex ||
decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1
) {
keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex];
if (decryptdata.pssh) {
break;
let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex];
for (let i = 0; i < mediaKeySessions.length; i++) {
// Match playlist key
const keyContext = mediaKeySessions[i];
const decryptdata = keyContext.decryptdata;
if (!decryptdata.keyId) {
continue;
}
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;
}
}

if (!keySessionContextPromise) {
// Clear-lead key (not encountered in playlist)
keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
this.getKeySystemSelectionPromise([keySystemDomain]).then(
({ keySystem, mediaKeys }) => {
this.throwIfDestroyed();
const decryptdata = new LevelKey(
'ISO-23001-7',
keyIdHex,
keySystemToKeySystemFormat(keySystem) ?? '',
);
decryptdata.pssh = new Uint8Array(initData);
decryptdata.keyId = keyId as Uint8Array;
return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => {
this.throwIfDestroyed();
const keySessionContext = this.createMediaKeySessionContext({
decryptdata,
keySystem,
mediaKeys,
});
const oldKeyIdHex = Hex.hexDump(decryptdata.keyId);
if (
keyIdHex === oldKeyIdHex ||
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;
keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
keySessionContextPromise.then(() => {
return this.generateRequestWithPreferredKeySession(
keySessionContext,
keyContext,
initDataType,
initData,
'encrypted-event-no-match',
'encrypted-event-key-match',
);
});
},
);
keySessionContextPromise.catch((error) => this.handleError(error));
}
keySessionContextPromise.catch((error) => this.handleError(error));
break;
}
}

if (!keySessionContextPromise && keySystemDomain === keySystem) {
// "Clear-lead" (misc key not encountered in playlist)
keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
this.getKeySystemSelectionPromise([keySystemDomain]).then(
({ keySystem, mediaKeys }) => {
this.throwIfDestroyed();

const decryptdata = new LevelKey(
'ISO-23001-7',
keyIdHex,
keySystemToKeySystemFormat(keySystem) ?? '',
);
decryptdata.pssh = new Uint8Array(initData);
decryptdata.keyId = keyId as Uint8Array;
return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => {
this.throwIfDestroyed();
const keySessionContext = this.createMediaKeySessionContext({
decryptdata,
keySystem,
mediaKeys,
});
return this.generateRequestWithPreferredKeySession(
keySessionContext,
initDataType,
initData,
'encrypted-event-no-match',
);
});
},
);

keySessionContextPromise.catch((error) => this.handleError(error));
}
});
};

private onWaitingForKey = (event: Event) => {
Expand Down
7 changes: 6 additions & 1 deletion src/loader/key-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,12 @@ export default class KeyLoader implements ComponentAPI {
}

load(frag: Fragment): Promise<KeyLoadedData> {
if (!frag.decryptdata && frag.encrypted && this.emeController) {
if (
!frag.decryptdata &&
frag.encrypted &&
this.emeController &&
this.config.emeEnabled
) {
// Multiple keys, but none selected, resolve in eme-controller
return this.emeController
.selectKeySystemFormat(frag)
Expand Down
26 changes: 22 additions & 4 deletions tests/unit/controller/eme-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import sinonChai from 'sinon-chai';
import EMEController from '../../../src/controller/eme-controller';
import { ErrorDetails } from '../../../src/errors';
import { Events } from '../../../src/events';
import { KeySystemFormats } from '../../../src/utils/mediakeys-helper';
import HlsMock from '../../mocks/hls.mock';
import type { MediaKeySessionContext } from '../../../src/controller/eme-controller';
import type { MediaAttachedData } from '../../../src/types/events';
Expand Down Expand Up @@ -266,6 +267,11 @@ describe('EMEController', function () {
setupEach({
emeEnabled: true,
requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy,
drmSystems: {
'com.apple.fps': {
licenseUrl: '.',
},
},
});

const badData = {
Expand Down Expand Up @@ -293,10 +299,22 @@ describe('EMEController', function () {

media.emit('encrypted', badData);

expect(emeController.keyIdToKeySessionPromise).to.deep.equal(
{},
'`keyIdToKeySessionPromise` should be an empty dictionary when no key IDs are found',
);
return emeController
.selectKeySystemFormat({
levelkeys: {
[KeySystemFormats.FAIRPLAY]: {},
[KeySystemFormats.WIDEVINE]: {},
[KeySystemFormats.PLAYREADY]: {},
},
sn: 0,
type: 'main',
} as any)
.then(() => {
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 98f7e26

Please sign in to comment.