hls.js 1.6.7-0.canary.11366 → 1.6.7-0.canary.11369

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -127,7 +127,7 @@
127
127
  "rollup": "4.44.1",
128
128
  "rollup-plugin-istanbul": "5.0.0",
129
129
  "sauce-connect-launcher": "1.3.2",
130
- "selenium-webdriver": "4.33.0",
130
+ "selenium-webdriver": "4.34.0",
131
131
  "semver": "7.7.2",
132
132
  "sinon": "19.0.5",
133
133
  "sinon-chai": "3.7.0",
@@ -135,5 +135,5 @@
135
135
  "url-toolkit": "2.2.5",
136
136
  "wrangler": "4.22.0"
137
137
  },
138
- "version": "1.6.7-0.canary.11366"
138
+ "version": "1.6.7-0.canary.11369"
139
139
  }
@@ -201,9 +201,7 @@ class AudioStreamController
201
201
  const syncFrag = findNearestWithCC(trackDetails, targetDiscontinuity, pos);
202
202
  // Only stop waiting for audioFrag.cc if an audio segment of the same discontinuity domain (cc) is found
203
203
  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}`);
207
205
  this.startFragRequested = false;
208
206
  this.nextLoadPosition = syncFrag.start;
209
207
  this.resetLoadingState();
@@ -17,21 +17,11 @@ import {
17
17
  getKeySystemsForConfig,
18
18
  getSupportedMediaKeySystemConfigurations,
19
19
  isPersistentSessionType,
20
+ keySystemDomainToKeySystemFormat,
20
21
  keySystemFormatToKeySystemDomain,
21
- keySystemIdToKeySystemDomain,
22
22
  KeySystems,
23
- keySystemDomainToKeySystemFormat as keySystemToKeySystemFormat,
24
- parsePlayReadyWRM,
25
23
  requestMediaKeySystemAccess,
26
24
  } 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';
35
25
  import { stringify } from '../utils/safe-json-stringify';
36
26
  import { strToUtf8array } from '../utils/utf8-utils';
37
27
  import type { EMEControllerConfig, HlsConfig, LoadPolicy } from '../config';
@@ -119,7 +109,7 @@ class EMEController extends Logger implements ComponentAPI {
119
109
  // @ts-ignore
120
110
  this.hls = this.config = this.keyIdToKeySessionPromise = null;
121
111
  // @ts-ignore
122
- this.onMediaEncrypted = this.onWaitingForKey = null;
112
+ this.onWaitingForKey = null;
123
113
  }
124
114
 
125
115
  private registerListeners() {
@@ -398,7 +388,7 @@ class EMEController extends Logger implements ComponentAPI {
398
388
  hasMediaKeys: this.keySystemAccessPromises[keySystem].hasMediaKeys,
399
389
  }))
400
390
  .filter(({ hasMediaKeys }) => !!hasMediaKeys)
401
- .map(({ keySystem }) => keySystemToKeySystemFormat(keySystem))
391
+ .map(({ keySystem }) => keySystemDomainToKeySystemFormat(keySystem))
402
392
  .filter((keySystem): keySystem is KeySystemFormats => !!keySystem);
403
393
  }
404
394
 
@@ -416,7 +406,7 @@ class EMEController extends Logger implements ComponentAPI {
416
406
  return new Promise((resolve, reject) => {
417
407
  return this.getKeySystemSelectionPromise(keySystemsToAttempt)
418
408
  .then(({ keySystem }) => {
419
- const keySystemFormat = keySystemToKeySystemFormat(keySystem);
409
+ const keySystemFormat = keySystemDomainToKeySystemFormat(keySystem);
420
410
  if (keySystemFormat) {
421
411
  resolve(keySystemFormat);
422
412
  } else {
@@ -562,197 +552,6 @@ class EMEController extends Logger implements ComponentAPI {
562
552
  return this.attemptKeySystemAccess(keySystemsToAttempt);
563
553
  }
564
554
 
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
-
756
555
  private onWaitingForKey = (event: Event) => {
757
556
  this.log(`"${event.type}" event`);
758
557
  };
@@ -1331,7 +1130,6 @@ class EMEController extends Logger implements ComponentAPI {
1331
1130
  // keep reference of media
1332
1131
  this.media = media;
1333
1132
 
1334
- addEventListener(media, 'encrypted', this.onMediaEncrypted);
1335
1133
  addEventListener(media, 'waitingforkey', this.onWaitingForKey);
1336
1134
  }
1337
1135
 
@@ -1339,7 +1137,6 @@ class EMEController extends Logger implements ComponentAPI {
1339
1137
  const media = this.media;
1340
1138
 
1341
1139
  if (media) {
1342
- removeEventListener(media, 'encrypted', this.onMediaEncrypted);
1343
1140
  removeEventListener(media, 'waitingforkey', this.onWaitingForKey);
1344
1141
  this.media = null;
1345
1142
  this.mediaKeys = null;
@@ -6,6 +6,7 @@ import {
6
6
  isSCTE35Attribute,
7
7
  } from '../loader/date-range';
8
8
  import { MetadataSchema } from '../types/demuxer';
9
+ import { hexToArrayBuffer } from '../utils/hex';
9
10
  import { stringify } from '../utils/safe-json-stringify';
10
11
  import {
11
12
  clearCurrentCues,
@@ -73,15 +74,6 @@ const MAX_CUE_ENDTIME = (() => {
73
74
  return Number.POSITIVE_INFINITY;
74
75
  })();
75
76
 
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
- }
85
77
  class ID3TrackController implements ComponentAPI {
86
78
  private hls: Hls;
87
79
  private id3Track: TextTrack | null = null;
@@ -1,4 +1,5 @@
1
1
  import { isFullSegmentEncryption } from '../utils/encryption-methods-util';
2
+ import { hexToArrayBuffer } from '../utils/hex';
2
3
  import { convertDataUriToArrayBytes } from '../utils/keysystem-util';
3
4
  import { logger } from '../utils/logger';
4
5
  import { KeySystemFormats, parsePlayReadyWRM } from '../utils/mediakeys-helper';
@@ -41,6 +42,7 @@ export class LevelKey implements DecryptData {
41
42
  format: string,
42
43
  formatversions: number[] = [1],
43
44
  iv: Uint8Array<ArrayBuffer> | null = null,
45
+ keyId?: string,
44
46
  ) {
45
47
  this.method = method;
46
48
  this.uri = uri;
@@ -50,6 +52,20 @@ export class LevelKey implements DecryptData {
50
52
  this.encrypted = method ? method !== 'NONE' : false;
51
53
  this.isCommonEncryption =
52
54
  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
+ );
53
69
  }
54
70
 
55
71
  public isSupported(): boolean {
@@ -113,6 +129,10 @@ export class LevelKey implements DecryptData {
113
129
  return this;
114
130
  }
115
131
 
132
+ if (this.pssh && this.keyId) {
133
+ return this;
134
+ }
135
+
116
136
  // Initialize keyId if possible
117
137
  const keyBytes = convertDataUriToArrayBytes(this.uri);
118
138
  if (keyBytes) {
@@ -121,12 +141,10 @@ export class LevelKey implements DecryptData {
121
141
  // Setting `pssh` on this LevelKey/DecryptData allows HLS.js to generate a session using
122
142
  // the playlist-key before the "encrypted" event. (Comment out to only use "encrypted" path.)
123
143
  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);
130
148
  }
131
149
  break;
132
150
  case KeySystemFormats.PLAYREADY: {
@@ -572,10 +572,14 @@ export default class M3U8Parser {
572
572
  if (!levelkeys) {
573
573
  levelkeys = {};
574
574
  }
575
- if (levelkeys[levelKey.keyFormat]) {
576
- levelkeys = Object.assign({}, levelkeys);
575
+ const currentKey = levelkeys[levelKey.keyFormat];
576
+ // Ignore duplicate playlist KEY tags
577
+ if (!currentKey?.matches(levelKey)) {
578
+ if (currentKey) {
579
+ levelkeys = Object.assign({}, levelkeys);
580
+ }
581
+ levelkeys[levelKey.keyFormat] = levelKey;
577
582
  }
578
- levelkeys[levelKey.keyFormat] = levelKey;
579
583
  } else {
580
584
  logger.warn(`[Keys] Ignoring invalid EXT-X-KEY tag: "${value1}"`);
581
585
  }
@@ -856,6 +860,7 @@ function parseKey(
856
860
  decryptkeyformat,
857
861
  keyFormatVersions,
858
862
  decryptiv,
863
+ keyAttrs.KEYID,
859
864
  );
860
865
  }
861
866
 
package/src/utils/hex.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  const Hex = {
6
- hexDump: function (array: Uint8Array) {
6
+ hexDump: function (array: Uint8Array<ArrayBuffer>) {
7
7
  let str = '';
8
8
  for (let i = 0; i < array.length; i++) {
9
9
  let h = array[i].toString(16);
@@ -17,4 +17,14 @@ const Hex = {
17
17
  },
18
18
  };
19
19
 
20
+ export function hexToArrayBuffer(str: string): ArrayBuffer {
21
+ return Uint8Array.from(
22
+ str
23
+ .replace(/^0x/, '')
24
+ .replace(/([\da-fA-F]{2}) ?/g, '0x$1 ')
25
+ .replace(/ +$/, '')
26
+ .split(' '),
27
+ ).buffer;
28
+ }
29
+
20
30
  export default Hex;
@@ -177,7 +177,7 @@ export function isPersistentSessionType(
177
177
  );
178
178
  }
179
179
 
180
- export function parsePlayReadyWRM(keyBytes: Uint8Array) {
180
+ export function parsePlayReadyWRM(keyBytes: Uint8Array<ArrayBuffer>) {
181
181
  const keyBytesUtf16 = new Uint16Array(
182
182
  keyBytes.buffer,
183
183
  keyBytes.byteOffset,
@@ -590,7 +590,7 @@ export function patchEncyptionData(
590
590
  const tenc = parseSinf(sinf);
591
591
  if (tenc) {
592
592
  // Look for default key id (keyID offset is always 8 within the tenc box):
593
- const tencKeyId = tenc.subarray(8, 24);
593
+ const tencKeyId = tenc.subarray(8, 24) as Uint8Array<ArrayBuffer>;
594
594
  if (!tencKeyId.some((b) => b !== 0)) {
595
595
  logger.log(
596
596
  `[eme] Patching keyId in 'enc${
@@ -1353,8 +1353,8 @@ export function mp4pssh(
1353
1353
  export type PsshData = {
1354
1354
  version: 0 | 1;
1355
1355
  systemId: KeySystemIds;
1356
- kids: null | Uint8Array[];
1357
- data: null | Uint8Array;
1356
+ kids: null | Uint8Array<ArrayBuffer>[];
1357
+ data: null | Uint8Array<ArrayBuffer>;
1358
1358
  offset: number;
1359
1359
  size: number;
1360
1360
  };
@@ -1382,7 +1382,7 @@ export function parseMultiPssh(
1382
1382
  return results;
1383
1383
  }
1384
1384
 
1385
- function parsePssh(view: DataView): PsshData | PsshInvalidResult {
1385
+ function parsePssh(view: DataView<ArrayBuffer>): PsshData | PsshInvalidResult {
1386
1386
  const size = view.getUint32(0);
1387
1387
  const offset = view.byteOffset;
1388
1388
  const length = view.byteLength;
@@ -1405,8 +1405,8 @@ function parsePssh(view: DataView): PsshData | PsshInvalidResult {
1405
1405
  new Uint8Array(buffer, offset + 12, 16),
1406
1406
  ) as KeySystemIds;
1407
1407
 
1408
- let kids: null | Uint8Array[] = null;
1409
- let data: null | Uint8Array = null;
1408
+ let kids: null | Uint8Array<ArrayBuffer>[] = null;
1409
+ let data: null | Uint8Array<ArrayBuffer> = null;
1410
1410
  let dataSizeOffset = 0;
1411
1411
 
1412
1412
  if (version === 0) {