Skip to content

Commit 6f91d50

Browse files
committed
Use Widevine KEYID or parse Playready when level keys are present
Update keyUriToKeyIdMap set after KEY_LOADING Fixes video-dev#7541 video-dev#7542
1 parent 785c0a5 commit 6f91d50

File tree

4 files changed

+119
-16
lines changed

4 files changed

+119
-16
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3359,7 +3359,9 @@ export class LevelKey implements DecryptData {
33593359
// (undocumented)
33603360
readonly encrypted: boolean;
33613361
// (undocumented)
3362-
getDecryptData(sn: number | 'initSegment'): LevelKey | null;
3362+
getDecryptData(sn: number | 'initSegment', levelKeys?: {
3363+
[key: string]: LevelKey | undefined;
3364+
}): LevelKey | null;
33633365
// (undocumented)
33643366
readonly isCommonEncryption: boolean;
33653367
// (undocumented)

src/loader/fragment.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ export class Fragment extends BaseSegment {
273273
const levelKey = (this._decryptdata =
274274
this.levelkeys[keyFormats[0]] || null);
275275
if (levelKey) {
276-
return levelKey.getDecryptData(this.sn);
276+
return levelKey.getDecryptData(this.sn, this.levelkeys);
277277
}
278278
} else {
279279
// Multiple keys. key-loader to call Fragment.setKeyFormat based on selected key-system.
@@ -364,10 +364,11 @@ export class Fragment extends BaseSegment {
364364
}
365365

366366
setKeyFormat(keyFormat: KeySystemFormats) {
367-
if (this.levelkeys) {
368-
const key = this.levelkeys[keyFormat];
367+
const levelkeys = this.levelkeys;
368+
if (levelkeys) {
369+
const key = levelkeys[keyFormat];
369370
if (key && !this._decryptdata) {
370-
this._decryptdata = key.getDecryptData(this.sn);
371+
this._decryptdata = key.getDecryptData(this.sn, this.levelkeys);
371372
}
372373
}
373374
}

src/loader/level-key.ts

Lines changed: 56 additions & 11 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,11 @@ 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);
172+
if (!this.keyId) {
173+
const offset = keyBytes.length - 22;
174+
this.keyId = keyBytes.subarray(offset, offset + 16);
175+
}
161176
}
162177
break;
163178
case KeySystemFormats.PLAYREADY: {
@@ -189,13 +204,20 @@ export class LevelKey implements DecryptData {
189204

190205
// Default behavior: assign a new keyId for each uri
191206
if (!this.keyId || this.keyId.byteLength !== 16) {
192-
let keyId = keyUriToKeyIdMap[this.uri];
207+
let keyId: Uint8Array<ArrayBuffer> | null | undefined =
208+
keyUriToKeyIdMap[this.uri];
193209
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);
210+
keyId = getKeyIdFromWidevineKey(levelKeys);
211+
if (!keyId) {
212+
keyId = getKeyIdFromPlayReadyKey(levelKeys);
213+
if (!keyId) {
214+
const val =
215+
Object.keys(keyUriToKeyIdMap).length % Number.MAX_SAFE_INTEGER;
216+
keyId = new Uint8Array(16);
217+
const dv = new DataView(keyId.buffer, 12, 4); // Just set the last 4 bytes
218+
dv.setUint32(0, val);
219+
}
220+
}
199221
LevelKey.setKeyIdForUri(this.uri, keyId);
200222
}
201223
this.keyId = keyId;
@@ -205,6 +227,29 @@ export class LevelKey implements DecryptData {
205227
}
206228
}
207229

230+
function getKeyIdFromWidevineKey(
231+
levelKeys: { [key: string]: LevelKey | undefined } | undefined,
232+
) {
233+
const widevineKey = levelKeys?.[KeySystemFormats.WIDEVINE];
234+
if (widevineKey) {
235+
return widevineKey.keyId;
236+
}
237+
return null;
238+
}
239+
240+
function getKeyIdFromPlayReadyKey(
241+
levelKeys: { [key: string]: LevelKey | undefined } | undefined,
242+
) {
243+
const playReadyKey = levelKeys?.[KeySystemFormats.PLAYREADY];
244+
if (playReadyKey) {
245+
const playReadyKeyBytes = convertDataUriToArrayBytes(playReadyKey.uri);
246+
if (playReadyKeyBytes) {
247+
return parsePlayReadyWRM(playReadyKeyBytes);
248+
}
249+
}
250+
return null;
251+
}
252+
208253
function createInitializationVector(segmentNumber: number) {
209254
const uint8View = new Uint8Array(16);
210255
for (let i = 12; i < 16; i++) {

tests/unit/loader/m3u8-parser.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { LoadStats } from '../../../src/loader/load-stats';
44
import M3U8Parser from '../../../src/loader/m3u8-parser';
55
import { PlaylistLevelType } from '../../../src/types/loader';
66
import { AttrList } from '../../../src/utils/attr-list';
7+
import { arrayToHex } from '../../../src/utils/hex';
8+
import { KeySystemFormats } from '../../../src/utils/mediakeys-helper';
79
import type { Fragment, Part } from '../../../src/loader/fragment';
810
import type { LevelKey } from '../../../src/loader/level-key';
911

@@ -2414,6 +2416,59 @@ media_1638278.m4s`;
24142416
.which.has.members([result.fragments[2], result.fragments[6]]);
24152417
});
24162418

2419+
it('parse KEYID other keys when available', function () {
2420+
const level = `#EXTM3U
2421+
#EXT-X-VERSION:6
2422+
#EXT-X-TARGETDURATION:6
2423+
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://any",KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1"
2424+
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="data:text/plain;base64,AAAA",KEYID=0x50906c6be9179460c3d1a59f16ee9804,KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",KEYFORMATVERSIONS="1"
2425+
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="data:text/plain;charset=UTF-16;base64,UAIA",KEYFORMATVERSIONS="1",KEYFORMAT="com.microsoft.playready"
2426+
#EXT-X-MAP:URI="init.mp4"
2427+
#EXTINF:5.0,
2428+
1.mp4`;
2429+
const result = M3U8Parser.parseLevelPlaylist(
2430+
level,
2431+
'http://foo.com/adaptive/test.m3u8',
2432+
0,
2433+
PlaylistLevelType.MAIN,
2434+
0,
2435+
null,
2436+
);
2437+
expect(result.playlistParsingError).to.be.null;
2438+
expect(result.fragments.length).to.equal(1);
2439+
expect(result.fragments[0].levelkeys, 'first segment has three keys')
2440+
.to.be.an('object')
2441+
.with.keys([
2442+
'com.apple.streamingkeydelivery',
2443+
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
2444+
'com.microsoft.playready',
2445+
]);
2446+
const fragment = result.fragments[0];
2447+
expect(result)
2448+
.to.have.property('encryptedFragments')
2449+
.which.is.an('array')
2450+
.which.has.members([fragment]);
2451+
expect(
2452+
fragment.decryptdata,
2453+
'decryptdata should by null until format is selected',
2454+
).to.be.null;
2455+
fragment.setKeyFormat(KeySystemFormats.FAIRPLAY);
2456+
expect(fragment.decryptdata)
2457+
.to.include({
2458+
uri: 'skd://any',
2459+
method: 'SAMPLE-AES',
2460+
keyFormat: 'com.apple.streamingkeydelivery',
2461+
encrypted: true,
2462+
isCommonEncryption: true,
2463+
pssh: null,
2464+
})
2465+
.and.have.property('keyId')
2466+
.which.is.a('Uint8Array');
2467+
expect(arrayToHex(fragment.decryptdata!.keyId!)).to.equal(
2468+
'50906c6be9179460c3d1a59f16ee9804',
2469+
);
2470+
});
2471+
24172472
it('parses manifest with EXT-X-SESSION-KEYs', function () {
24182473
const manifest = `#EXTM3U
24192474
#EXT-X-SESSION-DATA:DATA-ID="key",VALUE="value"

0 commit comments

Comments
 (0)