Skip to content

Commit e3e9659

Browse files
committed
Get Widevine key id from EXT-X-KEY KEYID
Fixes video-dev#7369
1 parent 9c0d46d commit e3e9659

File tree

9 files changed

+59
-237
lines changed

9 files changed

+59
-237
lines changed

api-extractor/report/hls.js.api.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3339,7 +3339,7 @@ export class LevelDetails {
33393339
//
33403340
// @public (undocumented)
33413341
export class LevelKey implements DecryptData {
3342-
constructor(method: string, uri: string, format: string, formatversions?: number[], iv?: Uint8Array<ArrayBuffer> | null);
3342+
constructor(method: string, uri: string, format: string, formatversions?: number[], iv?: Uint8Array<ArrayBuffer> | null, keyId?: string);
33433343
// (undocumented)
33443344
static clearKeyUriToKeyIdMap(): void;
33453345
// (undocumented)
@@ -3361,6 +3361,8 @@ export class LevelKey implements DecryptData {
33613361
// (undocumented)
33623362
keyId: Uint8Array<ArrayBuffer> | null;
33633363
// (undocumented)
3364+
matches(key: LevelKey): boolean;
3365+
// (undocumented)
33643366
readonly method: string;
33653367
// (undocumented)
33663368
pssh: Uint8Array<ArrayBuffer> | null;

src/controller/audio-stream-controller.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,7 @@ class AudioStreamController
201201
const syncFrag = findNearestWithCC(trackDetails, targetDiscontinuity, pos);
202202
// Only stop waiting for audioFrag.cc if an audio segment of the same discontinuity domain (cc) is found
203203
if (syncFrag) {
204-
this.log(
205-
`Waiting fragment cc (${waitingToAppend?.cc}) cancelled because video is at cc ${mainAnchor.cc}`,
206-
);
204+
this.log(`Syncing with main frag at ${syncFrag.start} cc ${syncFrag.cc}`);
207205
this.startFragRequested = false;
208206
this.nextLoadPosition = syncFrag.start;
209207
this.resetLoadingState();

src/controller/eme-controller.ts

Lines changed: 4 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,11 @@ import {
1717
getKeySystemsForConfig,
1818
getSupportedMediaKeySystemConfigurations,
1919
isPersistentSessionType,
20+
keySystemDomainToKeySystemFormat,
2021
keySystemFormatToKeySystemDomain,
21-
keySystemIdToKeySystemDomain,
2222
KeySystems,
23-
keySystemDomainToKeySystemFormat as keySystemToKeySystemFormat,
24-
parsePlayReadyWRM,
2523
requestMediaKeySystemAccess,
2624
} from '../utils/mediakeys-helper';
27-
import {
28-
bin2str,
29-
parseMultiPssh,
30-
parseSinf,
31-
type PsshData,
32-
type PsshInvalidResult,
33-
} from '../utils/mp4-tools';
34-
import { base64Decode } from '../utils/numeric-encoding-utils';
3525
import { stringify } from '../utils/safe-json-stringify';
3626
import { strToUtf8array } from '../utils/utf8-utils';
3727
import type { EMEControllerConfig, HlsConfig, LoadPolicy } from '../config';
@@ -119,7 +109,7 @@ class EMEController extends Logger implements ComponentAPI {
119109
// @ts-ignore
120110
this.hls = this.config = this.keyIdToKeySessionPromise = null;
121111
// @ts-ignore
122-
this.onMediaEncrypted = this.onWaitingForKey = null;
112+
this.onWaitingForKey = null;
123113
}
124114

125115
private registerListeners() {
@@ -398,7 +388,7 @@ class EMEController extends Logger implements ComponentAPI {
398388
hasMediaKeys: this.keySystemAccessPromises[keySystem].hasMediaKeys,
399389
}))
400390
.filter(({ hasMediaKeys }) => !!hasMediaKeys)
401-
.map(({ keySystem }) => keySystemToKeySystemFormat(keySystem))
391+
.map(({ keySystem }) => keySystemDomainToKeySystemFormat(keySystem))
402392
.filter((keySystem): keySystem is KeySystemFormats => !!keySystem);
403393
}
404394

@@ -416,7 +406,7 @@ class EMEController extends Logger implements ComponentAPI {
416406
return new Promise((resolve, reject) => {
417407
return this.getKeySystemSelectionPromise(keySystemsToAttempt)
418408
.then(({ keySystem }) => {
419-
const keySystemFormat = keySystemToKeySystemFormat(keySystem);
409+
const keySystemFormat = keySystemDomainToKeySystemFormat(keySystem);
420410
if (keySystemFormat) {
421411
resolve(keySystemFormat);
422412
} else {
@@ -562,197 +552,6 @@ class EMEController extends Logger implements ComponentAPI {
562552
return this.attemptKeySystemAccess(keySystemsToAttempt);
563553
}
564554

565-
private onMediaEncrypted = (event: MediaEncryptedEvent) => {
566-
const { initDataType, initData } = event;
567-
const logMessage = `"${event.type}" event: init data type: "${initDataType}"`;
568-
this.debug(logMessage);
569-
570-
// Ignore event when initData is null
571-
if (initData === null) {
572-
return;
573-
}
574-
575-
if (!this.keyFormatPromise) {
576-
let keySystems = Object.keys(
577-
this.keySystemAccessPromises,
578-
) as KeySystems[];
579-
if (!keySystems.length) {
580-
keySystems = getKeySystemsForConfig(this.config);
581-
}
582-
const keyFormats = keySystems
583-
.map(keySystemToKeySystemFormat)
584-
.filter((k) => !!k) as KeySystemFormats[];
585-
this.keyFormatPromise = this.getKeyFormatPromise(keyFormats);
586-
}
587-
588-
this.keyFormatPromise.then((keySystemFormat) => {
589-
const keySystem = keySystemFormatToKeySystemDomain(keySystemFormat);
590-
591-
let keyId: Uint8Array<ArrayBuffer> | null | undefined;
592-
let keySystemDomain: KeySystems | undefined;
593-
594-
if (initDataType === 'sinf') {
595-
if (keySystem !== KeySystems.FAIRPLAY) {
596-
this.warn(
597-
`Ignoring unexpected "${event.type}" event with init data type: "${initDataType}" for selected key-system ${keySystem}`,
598-
);
599-
return;
600-
}
601-
// Match sinf keyId to playlist skd://keyId=
602-
const json = bin2str(new Uint8Array(initData));
603-
try {
604-
const sinf = base64Decode(JSON.parse(json).sinf);
605-
const tenc = parseSinf(sinf);
606-
if (!tenc) {
607-
throw new Error(
608-
`'schm' box missing or not cbcs/cenc with schi > tenc`,
609-
);
610-
}
611-
keyId = new Uint8Array(tenc.subarray(8, 24));
612-
keySystemDomain = KeySystems.FAIRPLAY;
613-
} catch (error) {
614-
this.warn(`${logMessage} Failed to parse sinf: ${error}`);
615-
return;
616-
}
617-
} else {
618-
if (
619-
keySystem !== KeySystems.WIDEVINE &&
620-
keySystem !== KeySystems.PLAYREADY
621-
) {
622-
this.warn(
623-
`Ignoring unexpected "${event.type}" event with init data type: "${initDataType}" for selected key-system ${keySystem}`,
624-
);
625-
return;
626-
}
627-
// Support Widevine/PlayReady clear-lead key-session creation (otherwise depend on playlist keys)
628-
const psshResults = parseMultiPssh(initData);
629-
630-
const psshInfos = psshResults.filter(
631-
(pssh): pssh is PsshData =>
632-
!!pssh.systemId &&
633-
keySystemIdToKeySystemDomain(pssh.systemId) === keySystem,
634-
);
635-
636-
if (psshInfos.length > 1) {
637-
this.warn(
638-
`${logMessage} Using first of ${psshInfos.length} pssh found for selected key-system ${keySystem}`,
639-
);
640-
}
641-
642-
const psshInfo = psshInfos[0];
643-
644-
if (!psshInfo) {
645-
if (
646-
psshResults.length === 0 ||
647-
psshResults.some(
648-
(pssh): pssh is PsshInvalidResult => !pssh.systemId,
649-
)
650-
) {
651-
this.warn(`${logMessage} contains incomplete or invalid pssh data`);
652-
} else {
653-
this.log(
654-
`ignoring ${logMessage} for ${(psshResults as PsshData[])
655-
.map((pssh) => keySystemIdToKeySystemDomain(pssh.systemId))
656-
.join(',')} pssh data in favor of playlist keys`,
657-
);
658-
}
659-
return;
660-
}
661-
662-
keySystemDomain = keySystemIdToKeySystemDomain(psshInfo.systemId);
663-
if (psshInfo.version === 0 && psshInfo.data) {
664-
if (keySystemDomain === KeySystems.WIDEVINE) {
665-
const offset = psshInfo.data.length - 22;
666-
keyId = new Uint8Array(psshInfo.data.subarray(offset, offset + 16));
667-
} else if (keySystemDomain === KeySystems.PLAYREADY) {
668-
keyId = parsePlayReadyWRM(psshInfo.data);
669-
}
670-
}
671-
}
672-
673-
if (!keySystemDomain || !keyId) {
674-
return;
675-
}
676-
677-
const keyIdHex = Hex.hexDump(keyId);
678-
const { keyIdToKeySessionPromise, mediaKeySessions } = this;
679-
680-
let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex];
681-
for (let i = 0; i < mediaKeySessions.length; i++) {
682-
// Match playlist key
683-
const keyContext = mediaKeySessions[i];
684-
const decryptdata = keyContext.decryptdata;
685-
if (!decryptdata.keyId) {
686-
continue;
687-
}
688-
const oldKeyIdHex = Hex.hexDump(decryptdata.keyId);
689-
if (
690-
keyIdHex === oldKeyIdHex ||
691-
decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1
692-
) {
693-
keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex];
694-
if (decryptdata.pssh) {
695-
break;
696-
}
697-
delete keyIdToKeySessionPromise[oldKeyIdHex];
698-
decryptdata.pssh = new Uint8Array(initData);
699-
decryptdata.keyId = keyId;
700-
keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
701-
keySessionContextPromise.then(() => {
702-
return this.generateRequestWithPreferredKeySession(
703-
keyContext,
704-
initDataType,
705-
initData,
706-
'encrypted-event-key-match',
707-
);
708-
});
709-
keySessionContextPromise.catch((error) => this.handleError(error));
710-
break;
711-
}
712-
}
713-
714-
if (!keySessionContextPromise) {
715-
if (keySystemDomain !== keySystem) {
716-
this.log(
717-
`Ignoring "${event.type}" event with ${keySystemDomain} init data for selected key-system ${keySystem}`,
718-
);
719-
return;
720-
}
721-
// "Clear-lead" (misc key not encountered in playlist)
722-
keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
723-
this.getKeySystemSelectionPromise([keySystemDomain]).then(
724-
({ keySystem, mediaKeys }) => {
725-
this.throwIfDestroyed();
726-
727-
const decryptdata = new LevelKey(
728-
'ISO-23001-7',
729-
keyIdHex,
730-
keySystemToKeySystemFormat(keySystem) ?? '',
731-
);
732-
decryptdata.pssh = new Uint8Array(initData);
733-
decryptdata.keyId = keyId;
734-
return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => {
735-
this.throwIfDestroyed();
736-
const keySessionContext = this.createMediaKeySessionContext({
737-
decryptdata,
738-
keySystem,
739-
mediaKeys,
740-
});
741-
return this.generateRequestWithPreferredKeySession(
742-
keySessionContext,
743-
initDataType,
744-
initData,
745-
'encrypted-event-no-match',
746-
);
747-
});
748-
},
749-
);
750-
751-
keySessionContextPromise.catch((error) => this.handleError(error));
752-
}
753-
});
754-
};
755-
756555
private onWaitingForKey = (event: Event) => {
757556
this.log(`"${event.type}" event`);
758557
};
@@ -1331,15 +1130,13 @@ class EMEController extends Logger implements ComponentAPI {
13311130
// keep reference of media
13321131
this.media = media;
13331132

1334-
addEventListener(media, 'encrypted', this.onMediaEncrypted);
13351133
addEventListener(media, 'waitingforkey', this.onWaitingForKey);
13361134
}
13371135

13381136
private onMediaDetached() {
13391137
const media = this.media;
13401138

13411139
if (media) {
1342-
removeEventListener(media, 'encrypted', this.onMediaEncrypted);
13431140
removeEventListener(media, 'waitingforkey', this.onWaitingForKey);
13441141
this.media = null;
13451142
this.mediaKeys = null;

src/controller/id3-track-controller.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
isSCTE35Attribute,
77
} from '../loader/date-range';
88
import { MetadataSchema } from '../types/demuxer';
9+
import { hexToArrayBuffer } from '../utils/hex';
910
import { stringify } from '../utils/safe-json-stringify';
1011
import {
1112
clearCurrentCues,
@@ -73,15 +74,6 @@ const MAX_CUE_ENDTIME = (() => {
7374
return Number.POSITIVE_INFINITY;
7475
})();
7576

76-
function hexToArrayBuffer(str): ArrayBuffer {
77-
return Uint8Array.from(
78-
str
79-
.replace(/^0x/, '')
80-
.replace(/([\da-fA-F]{2}) ?/g, '0x$1 ')
81-
.replace(/ +$/, '')
82-
.split(' '),
83-
).buffer;
84-
}
8577
class ID3TrackController implements ComponentAPI {
8678
private hls: Hls;
8779
private id3Track: TextTrack | null = null;

src/loader/level-key.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { isFullSegmentEncryption } from '../utils/encryption-methods-util';
2+
import { hexToArrayBuffer } from '../utils/hex';
23
import { convertDataUriToArrayBytes } from '../utils/keysystem-util';
34
import { logger } from '../utils/logger';
45
import { KeySystemFormats, parsePlayReadyWRM } from '../utils/mediakeys-helper';
@@ -41,6 +42,7 @@ export class LevelKey implements DecryptData {
4142
format: string,
4243
formatversions: number[] = [1],
4344
iv: Uint8Array<ArrayBuffer> | null = null,
45+
keyId?: string,
4446
) {
4547
this.method = method;
4648
this.uri = uri;
@@ -50,6 +52,20 @@ export class LevelKey implements DecryptData {
5052
this.encrypted = method ? method !== 'NONE' : false;
5153
this.isCommonEncryption =
5254
this.encrypted && !isFullSegmentEncryption(method);
55+
if (keyId?.startsWith('0x')) {
56+
this.keyId = new Uint8Array(hexToArrayBuffer(keyId));
57+
}
58+
}
59+
60+
public matches(key: LevelKey): boolean {
61+
return (
62+
key.uri === this.uri &&
63+
key.method === this.method &&
64+
key.encrypted === this.encrypted &&
65+
key.keyFormat === this.keyFormat &&
66+
key.keyFormatVersions.join(',') === this.keyFormatVersions.join(',') &&
67+
key.iv?.join(',') === this.iv?.join(',')
68+
);
5369
}
5470

5571
public isSupported(): boolean {
@@ -113,6 +129,10 @@ export class LevelKey implements DecryptData {
113129
return this;
114130
}
115131

132+
if (this.pssh && this.keyId) {
133+
return this;
134+
}
135+
116136
// Initialize keyId if possible
117137
const keyBytes = convertDataUriToArrayBytes(this.uri);
118138
if (keyBytes) {
@@ -121,12 +141,10 @@ export class LevelKey implements DecryptData {
121141
// Setting `pssh` on this LevelKey/DecryptData allows HLS.js to generate a session using
122142
// the playlist-key before the "encrypted" event. (Comment out to only use "encrypted" path.)
123143
this.pssh = keyBytes;
124-
// In case of widevine keyID is embedded in PSSH box. Read Key ID.
125-
if (keyBytes.length >= 22) {
126-
this.keyId = keyBytes.subarray(
127-
keyBytes.length - 22,
128-
keyBytes.length - 6,
129-
);
144+
// In case of Widevine, if KEYID is not in the playlist, assume only two fields in the pssh KEY tag URI.
145+
if (!this.keyId && keyBytes.length >= 22) {
146+
const offset = keyBytes.length - 22;
147+
this.keyId = keyBytes.subarray(offset, offset + 16);
130148
}
131149
break;
132150
case KeySystemFormats.PLAYREADY: {

0 commit comments

Comments
 (0)