Skip to content

Commit d602e36

Browse files
authored
Fix assumptions in Multi-DRM cases where only PlayReady is available for playback (#6946)
* fix: assumptions in multi-drm cases where only playready is available for playback. * Only use selected key-system in EME "encrypted" media event handler Resolves #6947 / Suggested changes for #6946 * chore: Refactors to improve logging in DRM mismatch cases. * chore: More updates to improve logging in DRM mismatch cases. * chore: Use a warn for unexpected psshInfos head count for key system
1 parent 1dee99c commit d602e36

File tree

3 files changed

+182
-121
lines changed

3 files changed

+182
-121
lines changed

src/controller/eme-controller.ts

+154-116
Original file line numberDiff line numberDiff line change
@@ -535,147 +535,185 @@ class EMEController extends Logger implements ComponentAPI {
535535
return;
536536
}
537537

538-
let keyId: Uint8Array | null | undefined;
539-
let keySystemDomain: KeySystems | undefined;
540-
541-
if (
542-
initDataType === 'sinf' &&
543-
this.getLicenseServerUrl(KeySystems.FAIRPLAY)
544-
) {
545-
// Match sinf keyId to playlist skd://keyId=
546-
const json = bin2str(new Uint8Array(initData));
547-
try {
548-
const sinf = base64Decode(JSON.parse(json).sinf);
549-
const tenc = parseSinf(sinf);
550-
if (!tenc) {
551-
throw new Error(
552-
`'schm' box missing or not cbcs/cenc with schi > tenc`,
553-
);
554-
}
555-
keyId = tenc.subarray(8, 24);
556-
keySystemDomain = KeySystems.FAIRPLAY;
557-
} catch (error) {
558-
this.warn(`${logMessage} Failed to parse sinf: ${error}`);
559-
return;
560-
}
561-
} else if (this.getLicenseServerUrl(KeySystems.WIDEVINE)) {
562-
// Support Widevine clear-lead key-session creation (otherwise depend on playlist keys)
563-
const psshResults = parseMultiPssh(initData);
564-
565-
// TODO: If using keySystemAccessPromises we might want to wait until one is resolved
538+
if (!this.keyFormatPromise) {
566539
let keySystems = Object.keys(
567540
this.keySystemAccessPromises,
568541
) as KeySystems[];
569542
if (!keySystems.length) {
570543
keySystems = getKeySystemsForConfig(this.config);
571544
}
545+
const keyFormats = keySystems
546+
.map(keySystemToKeySystemFormat)
547+
.filter((k) => !!k) as KeySystemFormats[];
548+
this.keyFormatPromise = this.getKeyFormatPromise(keyFormats);
549+
}
572550

573-
const psshInfo = psshResults.filter((pssh): pssh is PsshData => {
574-
const keySystem = pssh.systemId
575-
? keySystemIdToKeySystemDomain(pssh.systemId)
576-
: null;
577-
return keySystem ? keySystems.indexOf(keySystem) > -1 : false;
578-
})[0];
551+
this.keyFormatPromise.then((keySystemFormat) => {
552+
const keySystem = keySystemFormatToKeySystemDomain(keySystemFormat);
579553

580-
if (!psshInfo) {
554+
let keyId: Uint8Array | null | undefined;
555+
let keySystemDomain: KeySystems | undefined;
556+
557+
if (initDataType === 'sinf') {
558+
if (keySystem !== KeySystems.FAIRPLAY) {
559+
this.warn(
560+
`Ignoring unexpected "${event.type}" event with init data type: "${initDataType}" for selected key-system ${keySystem}`,
561+
);
562+
return;
563+
}
564+
// Match sinf keyId to playlist skd://keyId=
565+
const json = bin2str(new Uint8Array(initData));
566+
try {
567+
const sinf = base64Decode(JSON.parse(json).sinf);
568+
const tenc = parseSinf(sinf);
569+
if (!tenc) {
570+
throw new Error(
571+
`'schm' box missing or not cbcs/cenc with schi > tenc`,
572+
);
573+
}
574+
keyId = tenc.subarray(8, 24);
575+
keySystemDomain = KeySystems.FAIRPLAY;
576+
} catch (error) {
577+
this.warn(`${logMessage} Failed to parse sinf: ${error}`);
578+
return;
579+
}
580+
} else {
581581
if (
582-
psshResults.length === 0 ||
583-
psshResults.some((pssh): pssh is PsshInvalidResult => !pssh.systemId)
582+
keySystem !== KeySystems.WIDEVINE &&
583+
keySystem !== KeySystems.PLAYREADY
584584
) {
585-
this.warn(`${logMessage} contains incomplete or invalid pssh data`);
586-
} else {
587-
this.log(
588-
`ignoring ${logMessage} for ${(psshResults as PsshData[])
589-
.map((pssh) => keySystemIdToKeySystemDomain(pssh.systemId))
590-
.join(',')} pssh data in favor of playlist keys`,
585+
this.warn(
586+
`Ignoring unexpected "${event.type}" event with init data type: "${initDataType}" for selected key-system ${keySystem}`,
591587
);
588+
return;
592589
}
593-
return;
594-
}
590+
// Support Widevine/PlayReady clear-lead key-session creation (otherwise depend on playlist keys)
591+
const psshResults = parseMultiPssh(initData);
595592

596-
keySystemDomain = keySystemIdToKeySystemDomain(psshInfo.systemId);
597-
if (psshInfo.version === 0 && psshInfo.data) {
598-
if (keySystemDomain === KeySystems.WIDEVINE) {
599-
const offset = psshInfo.data.length - 22;
600-
keyId = psshInfo.data.subarray(offset, offset + 16);
601-
} else if (keySystemDomain === KeySystems.PLAYREADY) {
602-
keyId = parsePlayReadyWRM(psshInfo.data);
593+
const psshInfos = psshResults.filter(
594+
(pssh): pssh is PsshData =>
595+
!!pssh.systemId &&
596+
keySystemIdToKeySystemDomain(pssh.systemId) === keySystem,
597+
);
598+
599+
if (psshInfos.length) {
600+
this.warn(
601+
`${logMessage} Using first of ${psshInfos.length} pssh found for selected key-system ${keySystem}`,
602+
);
603603
}
604-
}
605-
}
606604

607-
if (!keySystemDomain || !keyId) {
608-
return;
609-
}
605+
const psshInfo = psshInfos[0];
610606

611-
const keyIdHex = Hex.hexDump(keyId);
612-
const { keyIdToKeySessionPromise, mediaKeySessions } = this;
607+
if (!psshInfo) {
608+
if (
609+
psshResults.length === 0 ||
610+
psshResults.some(
611+
(pssh): pssh is PsshInvalidResult => !pssh.systemId,
612+
)
613+
) {
614+
this.warn(`${logMessage} contains incomplete or invalid pssh data`);
615+
} else {
616+
this.log(
617+
`ignoring ${logMessage} for ${(psshResults as PsshData[])
618+
.map((pssh) => keySystemIdToKeySystemDomain(pssh.systemId))
619+
.join(',')} pssh data in favor of playlist keys`,
620+
);
621+
}
622+
return;
623+
}
613624

614-
let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex];
615-
for (let i = 0; i < mediaKeySessions.length; i++) {
616-
// Match playlist key
617-
const keyContext = mediaKeySessions[i];
618-
const decryptdata = keyContext.decryptdata;
619-
if (!decryptdata.keyId) {
620-
continue;
621-
}
622-
const oldKeyIdHex = Hex.hexDump(decryptdata.keyId);
623-
if (
624-
keyIdHex === oldKeyIdHex ||
625-
decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1
626-
) {
627-
keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex];
628-
if (decryptdata.pssh) {
629-
break;
625+
keySystemDomain = keySystemIdToKeySystemDomain(psshInfo.systemId);
626+
if (psshInfo.version === 0 && psshInfo.data) {
627+
if (keySystemDomain === KeySystems.WIDEVINE) {
628+
const offset = psshInfo.data.length - 22;
629+
keyId = psshInfo.data.subarray(offset, offset + 16);
630+
} else if (keySystemDomain === KeySystems.PLAYREADY) {
631+
keyId = parsePlayReadyWRM(psshInfo.data);
632+
}
630633
}
631-
delete keyIdToKeySessionPromise[oldKeyIdHex];
632-
decryptdata.pssh = new Uint8Array(initData);
633-
decryptdata.keyId = keyId;
634-
keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
635-
keySessionContextPromise.then(() => {
636-
return this.generateRequestWithPreferredKeySession(
637-
keyContext,
638-
initDataType,
639-
initData,
640-
'encrypted-event-key-match',
641-
);
642-
});
643-
keySessionContextPromise.catch((error) => this.handleError(error));
644-
break;
645634
}
646-
}
647635

648-
if (!keySessionContextPromise) {
649-
// Clear-lead key (not encountered in playlist)
650-
keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
651-
this.getKeySystemSelectionPromise([keySystemDomain]).then(
652-
({ keySystem, mediaKeys }) => {
653-
this.throwIfDestroyed();
654-
const decryptdata = new LevelKey(
655-
'ISO-23001-7',
656-
keyIdHex,
657-
keySystemToKeySystemFormat(keySystem) ?? '',
658-
);
659-
decryptdata.pssh = new Uint8Array(initData);
660-
decryptdata.keyId = keyId as Uint8Array;
661-
return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => {
662-
this.throwIfDestroyed();
663-
const keySessionContext = this.createMediaKeySessionContext({
664-
decryptdata,
665-
keySystem,
666-
mediaKeys,
667-
});
636+
if (!keySystemDomain || !keyId) {
637+
return;
638+
}
639+
640+
const keyIdHex = Hex.hexDump(keyId);
641+
const { keyIdToKeySessionPromise, mediaKeySessions } = this;
642+
643+
let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex];
644+
for (let i = 0; i < mediaKeySessions.length; i++) {
645+
// Match playlist key
646+
const keyContext = mediaKeySessions[i];
647+
const decryptdata = keyContext.decryptdata;
648+
if (!decryptdata.keyId) {
649+
continue;
650+
}
651+
const oldKeyIdHex = Hex.hexDump(decryptdata.keyId);
652+
if (
653+
keyIdHex === oldKeyIdHex ||
654+
decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1
655+
) {
656+
keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex];
657+
if (decryptdata.pssh) {
658+
break;
659+
}
660+
delete keyIdToKeySessionPromise[oldKeyIdHex];
661+
decryptdata.pssh = new Uint8Array(initData);
662+
decryptdata.keyId = keyId;
663+
keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
664+
keySessionContextPromise.then(() => {
668665
return this.generateRequestWithPreferredKeySession(
669-
keySessionContext,
666+
keyContext,
670667
initDataType,
671668
initData,
672-
'encrypted-event-no-match',
669+
'encrypted-event-key-match',
673670
);
674671
});
675-
},
676-
);
677-
keySessionContextPromise.catch((error) => this.handleError(error));
678-
}
672+
keySessionContextPromise.catch((error) => this.handleError(error));
673+
break;
674+
}
675+
}
676+
677+
if (!keySessionContextPromise) {
678+
if (keySystemDomain !== keySystem) {
679+
this.log(
680+
`Ignoring "${event.type}" event with ${keySystemDomain} init data for selected key-system ${keySystem}`,
681+
);
682+
return;
683+
}
684+
// "Clear-lead" (misc key not encountered in playlist)
685+
keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
686+
this.getKeySystemSelectionPromise([keySystemDomain]).then(
687+
({ keySystem, mediaKeys }) => {
688+
this.throwIfDestroyed();
689+
690+
const decryptdata = new LevelKey(
691+
'ISO-23001-7',
692+
keyIdHex,
693+
keySystemToKeySystemFormat(keySystem) ?? '',
694+
);
695+
decryptdata.pssh = new Uint8Array(initData);
696+
decryptdata.keyId = keyId as Uint8Array;
697+
return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => {
698+
this.throwIfDestroyed();
699+
const keySessionContext = this.createMediaKeySessionContext({
700+
decryptdata,
701+
keySystem,
702+
mediaKeys,
703+
});
704+
return this.generateRequestWithPreferredKeySession(
705+
keySessionContext,
706+
initDataType,
707+
initData,
708+
'encrypted-event-no-match',
709+
);
710+
});
711+
},
712+
);
713+
714+
keySessionContextPromise.catch((error) => this.handleError(error));
715+
}
716+
});
679717
};
680718

681719
private onWaitingForKey = (event: Event) => {

src/loader/key-loader.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,12 @@ export default class KeyLoader implements ComponentAPI {
112112
}
113113

114114
load(frag: Fragment): Promise<KeyLoadedData> {
115-
if (!frag.decryptdata && frag.encrypted && this.emeController) {
115+
if (
116+
!frag.decryptdata &&
117+
frag.encrypted &&
118+
this.emeController &&
119+
this.config.emeEnabled
120+
) {
116121
// Multiple keys, but none selected, resolve in eme-controller
117122
return this.emeController
118123
.selectKeySystemFormat(frag)

tests/unit/controller/eme-controller.ts

+22-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import sinonChai from 'sinon-chai';
55
import EMEController from '../../../src/controller/eme-controller';
66
import { ErrorDetails } from '../../../src/errors';
77
import { Events } from '../../../src/events';
8+
import { KeySystemFormats } from '../../../src/utils/mediakeys-helper';
89
import HlsMock from '../../mocks/hls.mock';
910
import type { MediaKeySessionContext } from '../../../src/controller/eme-controller';
1011
import type { MediaAttachedData } from '../../../src/types/events';
@@ -266,6 +267,11 @@ describe('EMEController', function () {
266267
setupEach({
267268
emeEnabled: true,
268269
requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy,
270+
drmSystems: {
271+
'com.apple.fps': {
272+
licenseUrl: '.',
273+
},
274+
},
269275
});
270276

271277
const badData = {
@@ -293,10 +299,22 @@ describe('EMEController', function () {
293299

294300
media.emit('encrypted', badData);
295301

296-
expect(emeController.keyIdToKeySessionPromise).to.deep.equal(
297-
{},
298-
'`keyIdToKeySessionPromise` should be an empty dictionary when no key IDs are found',
299-
);
302+
return emeController
303+
.selectKeySystemFormat({
304+
levelkeys: {
305+
[KeySystemFormats.FAIRPLAY]: {},
306+
[KeySystemFormats.WIDEVINE]: {},
307+
[KeySystemFormats.PLAYREADY]: {},
308+
},
309+
sn: 0,
310+
type: 'main',
311+
} as any)
312+
.then(() => {
313+
expect(emeController.keyIdToKeySessionPromise).to.deep.equal(
314+
{},
315+
'`keyIdToKeySessionPromise` should be an empty dictionary when no key IDs are found',
316+
);
317+
});
300318
});
301319

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

0 commit comments

Comments
 (0)