Skip to content

Commit bc96470

Browse files
committed
Merge branch 'patch/v1.6.x'
* patch/v1.6.x: Get KEYID from init segment 'tenc' when not found elsewhere Fixes video-dev#7541 Roll initPTS forward when needed by earlier segments Fixes video-dev#7536 Use Widevine KEYID or parse Playready when level keys are present Update keyUriToKeyIdMap set after KEY_LOADING Fixes video-dev#7541 video-dev#7542 Update npm to v11 before publish Use GitHub `release` env for npm publish (video-dev#7544) Fix "Missing format identifier #EXTM3U" playlist parsing errors Fixes video-dev#7531
2 parents ec14649 + abb6945 commit bc96470

File tree

7 files changed

+203
-71
lines changed

7 files changed

+203
-71
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3361,7 +3361,9 @@ export class LevelKey implements DecryptData {
33613361
// (undocumented)
33623362
readonly encrypted: boolean;
33633363
// (undocumented)
3364-
getDecryptData(sn: number | 'initSegment'): LevelKey | null;
3364+
getDecryptData(sn: number | 'initSegment', levelKeys?: {
3365+
[key: string]: LevelKey | undefined;
3366+
}): LevelKey | null;
33653367
// (undocumented)
33663368
readonly isCommonEncryption: boolean;
33673369
// (undocumented)

src/controller/eme-controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,7 @@ class EMEController extends Logger implements ComponentAPI {
877877
handleKeyStatus(keyStatus, context);
878878
} else {
879879
// Timeout key-status
880-
const timeout = 0;
880+
const timeout = 1000;
881881
context.keyStatusTimeouts ||= {};
882882
context.keyStatusTimeouts[keyId] ||= self.setTimeout(() => {
883883
if ((!context.mediaKeysSession as any) || !this.mediaKeys) {

src/loader/fragment.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -259,25 +259,24 @@ export class Fragment extends BaseSegment {
259259

260260
get decryptdata(): LevelKey | null {
261261
const { levelkeys } = this;
262-
if (!levelkeys && !this._decryptdata) {
262+
263+
if (!levelkeys || levelkeys.NONE) {
263264
return null;
264265
}
265266

266-
if (!this._decryptdata && this.levelkeys && !this.levelkeys.NONE) {
267-
const key = this.levelkeys.identity;
268-
if (key) {
269-
this._decryptdata = key.getDecryptData(this.sn);
270-
} else {
271-
const keyFormats = Object.keys(this.levelkeys);
272-
if (keyFormats.length === 1) {
273-
const levelKey = (this._decryptdata =
274-
this.levelkeys[keyFormats[0]] || null);
275-
if (levelKey) {
276-
return levelKey.getDecryptData(this.sn);
277-
}
278-
} else {
279-
// Multiple keys. key-loader to call Fragment.setKeyFormat based on selected key-system.
267+
if (levelkeys.identity) {
268+
if (!this._decryptdata) {
269+
this._decryptdata = levelkeys.identity.getDecryptData(this.sn);
270+
}
271+
} else if (!this._decryptdata?.keyId) {
272+
const keyFormats = Object.keys(levelkeys);
273+
if (keyFormats.length === 1) {
274+
const levelKey = (this._decryptdata = levelkeys[keyFormats[0]] || null);
275+
if (levelKey) {
276+
this._decryptdata = levelKey.getDecryptData(this.sn, levelkeys);
280277
}
278+
} else {
279+
// Multiple keys. key-loader to call Fragment.setKeyFormat based on selected key-system.
281280
}
282281
}
283282

@@ -364,10 +363,11 @@ export class Fragment extends BaseSegment {
364363
}
365364

366365
setKeyFormat(keyFormat: KeySystemFormats) {
367-
if (this.levelkeys) {
368-
const key = this.levelkeys[keyFormat];
369-
if (key && !this._decryptdata) {
370-
this._decryptdata = key.getDecryptData(this.sn);
366+
const levelkeys = this.levelkeys;
367+
if (levelkeys) {
368+
const key = levelkeys[keyFormat];
369+
if (key && !this._decryptdata?.keyId) {
370+
this._decryptdata = key.getDecryptData(this.sn, levelkeys);
371371
}
372372
}
373373
}

src/loader/key-loader.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { LoadError } from './fragment-loader';
2+
import { LevelKey } from './level-key';
23
import { ErrorDetails, ErrorTypes } from '../errors';
34
import { type Fragment, isMediaFragment } from '../loader/fragment';
45
import { arrayToHex } from '../utils/hex';
@@ -8,7 +9,7 @@ import {
89
keySystemFormatToKeySystemDomain,
910
} from '../utils/mediakeys-helper';
1011
import { KeySystemFormats } from '../utils/mediakeys-helper';
11-
import type { LevelKey } from './level-key';
12+
import { parseKeyIdsFromTenc } from '../utils/mp4-tools';
1213
import type { HlsConfig } from '../config';
1314
import type EMEController from '../controller/eme-controller';
1415
import type {
@@ -259,6 +260,19 @@ export default class KeyLoader extends Logger implements ComponentAPI {
259260
loadKeyEME(keyInfo: KeyLoaderInfo, frag: Fragment): Promise<KeyLoadedData> {
260261
const keyLoadedData: KeyLoadedData = { frag, keyInfo };
261262
if (this.emeController && this.config.emeEnabled) {
263+
if (!keyInfo.decryptdata.keyId && frag.initSegment?.data) {
264+
const keyIds = parseKeyIdsFromTenc(
265+
frag.initSegment.data as Uint8Array<ArrayBuffer>,
266+
);
267+
if (keyIds.length) {
268+
const keyId = keyIds[0];
269+
if (keyId.some((b) => b !== 0)) {
270+
this.log(`Using keyId found in init segment ${arrayToHex(keyId)}`);
271+
keyInfo.decryptdata.keyId = keyId;
272+
LevelKey.setKeyIdForUri(keyInfo.decryptdata.uri, keyId);
273+
}
274+
}
275+
}
262276
const keySessionContextPromise =
263277
this.emeController.loadKey(keyLoadedData);
264278
return (keyInfo.keyLoadPromise = keySessionContextPromise.then(

src/loader/level-key.ts

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@ export class LevelKey implements DecryptData {
100100
return false;
101101
}
102102

103-
public getDecryptData(sn: number | 'initSegment'): LevelKey | null {
103+
public getDecryptData(
104+
sn: number | 'initSegment',
105+
levelKeys?: { [key: string]: LevelKey | undefined },
106+
): LevelKey | null {
104107
if (!this.encrypted || !this.uri) {
105108
return null;
106109
}
@@ -135,10 +138,19 @@ export class LevelKey implements DecryptData {
135138
return this;
136139
}
137140

138-
if (this.pssh && this.keyId) {
139-
return this;
141+
if (this.keyId) {
142+
// Handle case where key id is changed in KEY_LOADING event handler #7542#issuecomment-3305203929
143+
const assignedKeyId = keyUriToKeyIdMap[this.uri];
144+
if (assignedKeyId && !arrayValuesMatch(this.keyId, assignedKeyId)) {
145+
LevelKey.setKeyIdForUri(this.uri, this.keyId);
146+
}
147+
148+
if (this.pssh) {
149+
return this;
150+
}
140151
}
141152

153+
// Key bytes are signalled the KEYID attribute, typically only found on WideVine KEY tags
142154
// Initialize keyId if possible
143155
const keyBytes = convertDataUriToArrayBytes(this.uri);
144156
if (keyBytes) {
@@ -156,8 +168,7 @@ export class LevelKey implements DecryptData {
156168
}
157169
}
158170
if (!this.keyId) {
159-
const offset = keyBytes.length - 22;
160-
this.keyId = keyBytes.subarray(offset, offset + 16);
171+
this.keyId = getKeyIdFromPlayReadyKey(levelKeys);
161172
}
162173
break;
163174
case KeySystemFormats.PLAYREADY: {
@@ -187,24 +198,49 @@ export class LevelKey implements DecryptData {
187198
}
188199
}
189200

190-
// Default behavior: assign a new keyId for each uri
201+
// Default behavior: get keyId from other KEY tag or URI lookup
191202
if (!this.keyId || this.keyId.byteLength !== 16) {
192-
let keyId = keyUriToKeyIdMap[this.uri];
203+
let keyId: Uint8Array<ArrayBuffer> | null | undefined;
204+
keyId = getKeyIdFromWidevineKey(levelKeys);
193205
if (!keyId) {
194-
const val =
195-
Object.keys(keyUriToKeyIdMap).length % Number.MAX_SAFE_INTEGER;
196-
keyId = new Uint8Array(16);
197-
const dv = new DataView(keyId.buffer, 12, 4); // Just set the last 4 bytes
198-
dv.setUint32(0, val);
206+
keyId = getKeyIdFromPlayReadyKey(levelKeys);
207+
if (!keyId) {
208+
keyId = keyUriToKeyIdMap[this.uri];
209+
}
210+
}
211+
if (keyId) {
212+
this.keyId = keyId;
199213
LevelKey.setKeyIdForUri(this.uri, keyId);
200214
}
201-
this.keyId = keyId;
202215
}
203216

204217
return this;
205218
}
206219
}
207220

221+
function getKeyIdFromWidevineKey(
222+
levelKeys: { [key: string]: LevelKey | undefined } | undefined,
223+
) {
224+
const widevineKey = levelKeys?.[KeySystemFormats.WIDEVINE];
225+
if (widevineKey) {
226+
return widevineKey.keyId;
227+
}
228+
return null;
229+
}
230+
231+
function getKeyIdFromPlayReadyKey(
232+
levelKeys: { [key: string]: LevelKey | undefined } | undefined,
233+
) {
234+
const playReadyKey = levelKeys?.[KeySystemFormats.PLAYREADY];
235+
if (playReadyKey) {
236+
const playReadyKeyBytes = convertDataUriToArrayBytes(playReadyKey.uri);
237+
if (playReadyKeyBytes) {
238+
return parsePlayReadyWRM(playReadyKeyBytes);
239+
}
240+
}
241+
return null;
242+
}
243+
208244
function createInitializationVector(segmentNumber: number) {
209245
const uint8View = new Uint8Array(16);
210246
for (let i = 12; i < 16; i++) {

src/utils/mp4-tools.ts

Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type { DecryptData } from '../loader/level-key';
77
import type { PassthroughTrack, UserdataSample } from '../types/demuxer';
88
import type { ILogger } from '../utils/logger';
99

10+
type BoxDataOrUndefined = Uint8Array<ArrayBuffer> | undefined;
11+
1012
const UINT32_MAX = Math.pow(2, 32) - 1;
1113
const push = [].push;
1214

@@ -573,51 +575,74 @@ export function patchEncyptionData(
573575
}
574576
const keyId = decryptdata.keyId;
575577
if (keyId && decryptdata.isCommonEncryption) {
576-
const traks = findBox(initSegment, ['moov', 'trak']);
577-
traks.forEach((trak) => {
578-
const stsd = findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0];
579-
580-
// skip the sample entry count
581-
const sampleEntries = stsd.subarray(8);
582-
let encBoxes = findBox(sampleEntries, ['enca']);
583-
const isAudio = encBoxes.length > 0;
584-
if (!isAudio) {
585-
encBoxes = findBox(sampleEntries, ['encv']);
578+
applyToTencBoxes(initSegment, (tenc, isAudio) => {
579+
// Look for default key id (keyID offset is always 8 within the tenc box):
580+
const tencKeyId = tenc.subarray(8, 24);
581+
if (!tencKeyId.some((b) => b !== 0)) {
582+
logger.log(
583+
`[eme] Patching keyId in 'enc${
584+
isAudio ? 'a' : 'v'
585+
}>sinf>>tenc' box: ${arrayToHex(tencKeyId)} -> ${arrayToHex(keyId)}`,
586+
);
587+
tenc.set(keyId, 8);
586588
}
587-
encBoxes.forEach((enc) => {
588-
const encBoxChildren = isAudio ? enc.subarray(28) : enc.subarray(78);
589-
const sinfBoxes = findBox(encBoxChildren, ['sinf']);
590-
sinfBoxes.forEach((sinf) => {
591-
const tenc = parseSinf(sinf);
592-
if (tenc) {
593-
// Look for default key id (keyID offset is always 8 within the tenc box):
594-
const tencKeyId = tenc.subarray(8, 24) as Uint8Array<ArrayBuffer>;
595-
if (!tencKeyId.some((b) => b !== 0)) {
596-
logger.log(
597-
`[eme] Patching keyId in 'enc${
598-
isAudio ? 'a' : 'v'
599-
}>sinf>>tenc' box: ${arrayToHex(tencKeyId)} -> ${arrayToHex(
600-
keyId,
601-
)}`,
602-
);
603-
tenc.set(keyId, 8);
604-
}
605-
}
606-
});
607-
});
608589
});
609590
}
610591
}
611592

612-
export function parseSinf(sinf: Uint8Array): Uint8Array | null {
613-
const schm = findBox(sinf, ['schm'])[0];
614-
if (schm as any) {
593+
export function parseKeyIdsFromTenc(
594+
initSegment: Uint8Array<ArrayBuffer>,
595+
): Uint8Array<ArrayBuffer>[] {
596+
const keyIds: Uint8Array<ArrayBuffer>[] = [];
597+
applyToTencBoxes(initSegment, (tenc) => keyIds.push(tenc.subarray(8, 24)));
598+
return keyIds;
599+
}
600+
601+
function applyToTencBoxes(
602+
initSegment: Uint8Array<ArrayBuffer>,
603+
predicate: (tenc: Uint8Array<ArrayBuffer>, isAudio: boolean) => void,
604+
) {
605+
const traks = findBox(initSegment, ['moov', 'trak']);
606+
traks.forEach((trak) => {
607+
const stsd = findBox(trak, [
608+
'mdia',
609+
'minf',
610+
'stbl',
611+
'stsd',
612+
])[0] as BoxDataOrUndefined;
613+
if (!stsd) return;
614+
const sampleEntries = stsd.subarray(8);
615+
let encBoxes = findBox(sampleEntries, ['enca']);
616+
const isAudio = encBoxes.length > 0;
617+
if (!isAudio) {
618+
encBoxes = findBox(sampleEntries, ['encv']);
619+
}
620+
encBoxes.forEach((enc) => {
621+
const encBoxChildren = isAudio ? enc.subarray(28) : enc.subarray(78);
622+
const sinfBoxes = findBox(encBoxChildren, ['sinf']);
623+
sinfBoxes.forEach((sinf) => {
624+
const tenc = parseSinf(sinf);
625+
if (tenc) {
626+
predicate(tenc, isAudio);
627+
}
628+
});
629+
});
630+
});
631+
}
632+
633+
export function parseSinf(sinf: Uint8Array): BoxDataOrUndefined {
634+
const schm = findBox(sinf, ['schm'])[0] as BoxDataOrUndefined;
635+
if (schm) {
615636
const scheme = bin2str(schm.subarray(4, 8));
616637
if (scheme === 'cbcs' || scheme === 'cenc') {
617-
return findBox(sinf, ['schi', 'tenc'])[0];
638+
const tenc = findBox(sinf, ['schi', 'tenc'])[0] as BoxDataOrUndefined;
639+
if (tenc) {
640+
return tenc;
641+
}
642+
} else if (scheme === 'cbc2') {
643+
/* no-op */
618644
}
619645
}
620-
return null;
621646
}
622647

623648
/*

0 commit comments

Comments
 (0)