hls.js 1.5.17 → 1.5.19

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
@@ -130,5 +130,5 @@
130
130
  "url-toolkit": "2.2.5",
131
131
  "wrangler": "3.22.4"
132
132
  },
133
- "version": "1.5.17"
133
+ "version": "1.5.19"
134
134
  }
@@ -281,12 +281,14 @@ class AudioStreamController
281
281
  const { hls, levels, media, trackId } = this;
282
282
  const config = hls.config;
283
283
 
284
- // 1. if video not attached AND
284
+ // 1. if buffering is suspended
285
+ // 2. if video not attached AND
285
286
  // start fragment already requested OR start frag prefetch not enabled
286
- // 2. if tracks or track not loaded and selected
287
+ // 3. if tracks or track not loaded and selected
287
288
  // then exit loop
288
289
  // => if media not attached but start frag prefetch is enabled and start frag not requested yet, we will not exit loop
289
290
  if (
291
+ !this.buffering ||
290
292
  (!media && (this.startFragRequested || !config.startFragPrefetch)) ||
291
293
  !levels?.[trackId]
292
294
  ) {
@@ -97,6 +97,7 @@ export default class BaseStreamController
97
97
  protected startFragRequested: boolean = false;
98
98
  protected decrypter: Decrypter;
99
99
  protected initPTS: RationalTimestamp[] = [];
100
+ protected buffering: boolean = true;
100
101
  protected onvseeking: EventListener | null = null;
101
102
  protected onvended: EventListener | null = null;
102
103
 
@@ -150,6 +151,14 @@ export default class BaseStreamController
150
151
  this.state = State.STOPPED;
151
152
  }
152
153
 
154
+ public pauseBuffering() {
155
+ this.buffering = false;
156
+ }
157
+
158
+ public resumeBuffering() {
159
+ this.buffering = true;
160
+ }
161
+
153
162
  protected _streamEnded(
154
163
  bufferInfo: BufferInfo,
155
164
  levelDetails: LevelDetails,
@@ -309,6 +309,7 @@ export default class BufferController implements ComponentAPI {
309
309
  this.resetBuffer(type);
310
310
  });
311
311
  this._initSourceBuffer();
312
+ this.hls.resumeBuffering();
312
313
  }
313
314
 
314
315
  private resetBuffer(type: SourceBufferName) {
@@ -9,12 +9,11 @@ import { logger } from '../utils/logger';
9
9
  import {
10
10
  getKeySystemsForConfig,
11
11
  getSupportedMediaKeySystemConfigurations,
12
- keySystemDomainToKeySystemFormat as keySystemToKeySystemFormat,
13
- KeySystemFormats,
14
12
  keySystemFormatToKeySystemDomain,
15
- KeySystemIds,
16
13
  keySystemIdToKeySystemDomain,
17
14
  KeySystems,
15
+ keySystemDomainToKeySystemFormat as keySystemToKeySystemFormat,
16
+ parsePlayReadyWRM,
18
17
  requestMediaKeySystemAccess,
19
18
  } from '../utils/mediakeys-helper';
20
19
  import { strToUtf8array } from '../utils/keysystem-util';
@@ -45,6 +44,7 @@ import type {
45
44
  LoaderConfiguration,
46
45
  LoaderContext,
47
46
  } from '../types/loader';
47
+ import type { KeySystemFormats } from '../utils/mediakeys-helper';
48
48
 
49
49
  const LOGGER_PREFIX = '[eme]';
50
50
 
@@ -94,8 +94,6 @@ class EMEController implements ComponentAPI {
94
94
  private setMediaKeysQueue: Promise<void>[] = EMEController.CDMCleanupPromise
95
95
  ? [EMEController.CDMCleanupPromise]
96
96
  : [];
97
- private onMediaEncrypted = this._onMediaEncrypted.bind(this);
98
- private onWaitingForKey = this._onWaitingForKey.bind(this);
99
97
 
100
98
  private debug: (msg: any) => void = logger.debug.bind(logger, LOGGER_PREFIX);
101
99
  private log: (msg: any) => void = logger.log.bind(logger, LOGGER_PREFIX);
@@ -117,13 +115,9 @@ class EMEController implements ComponentAPI {
117
115
  config.licenseXhrSetup = config.licenseResponseCallback = undefined;
118
116
  config.drmSystems = config.drmSystemOptions = {};
119
117
  // @ts-ignore
120
- this.hls =
121
- this.onMediaEncrypted =
122
- this.onWaitingForKey =
123
- this.keyIdToKeySessionPromise =
124
- null as any;
118
+ this.hls = this.config = this.keyIdToKeySessionPromise = null;
125
119
  // @ts-ignore
126
- this.config = null;
120
+ this.onMediaEncrypted = this.onWaitingForKey = null;
127
121
  }
128
122
 
129
123
  private registerListeners() {
@@ -140,7 +134,7 @@ class EMEController implements ComponentAPI {
140
134
  this.hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
141
135
  }
142
136
 
143
- private getLicenseServerUrl(keySystem: KeySystems): string | never {
137
+ private getLicenseServerUrl(keySystem: KeySystems): string | undefined {
144
138
  const { drmSystems, widevineLicenseUrl } = this.config;
145
139
  const keySystemConfiguration = drmSystems[keySystem];
146
140
 
@@ -152,10 +146,16 @@ class EMEController implements ComponentAPI {
152
146
  if (keySystem === KeySystems.WIDEVINE && widevineLicenseUrl) {
153
147
  return widevineLicenseUrl;
154
148
  }
149
+ }
155
150
 
156
- throw new Error(
157
- `no license server URL configured for key-system "${keySystem}"`,
158
- );
151
+ private getLicenseServerUrlOrThrow(keySystem: KeySystems): string | never {
152
+ const url = this.getLicenseServerUrl(keySystem);
153
+ if (url === undefined) {
154
+ throw new Error(
155
+ `no license server URL configured for key-system "${keySystem}"`,
156
+ );
157
+ }
158
+ return url;
159
159
  }
160
160
 
161
161
  private getServerCertificateUrl(keySystem: KeySystems): string | void {
@@ -527,7 +527,7 @@ class EMEController implements ComponentAPI {
527
527
  return this.attemptKeySystemAccess(keySystemsToAttempt);
528
528
  }
529
529
 
530
- private _onMediaEncrypted(event: MediaEncryptedEvent) {
530
+ private onMediaEncrypted = (event: MediaEncryptedEvent) => {
531
531
  const { initDataType, initData } = event;
532
532
  const logMessage = `"${event.type}" event: init data type: "${initDataType}"`;
533
533
  this.debug(logMessage);
@@ -537,133 +537,191 @@ class EMEController implements ComponentAPI {
537
537
  return;
538
538
  }
539
539
 
540
- let keyId: Uint8Array | undefined;
541
- let keySystemDomain: KeySystems | undefined;
540
+ if (!this.keyFormatPromise) {
541
+ let keySystems = Object.keys(
542
+ this.keySystemAccessPromises,
543
+ ) as KeySystems[];
544
+ if (!keySystems.length) {
545
+ keySystems = getKeySystemsForConfig(this.config);
546
+ }
547
+ const keyFormats = keySystems
548
+ .map(keySystemToKeySystemFormat)
549
+ .filter((k) => !!k) as KeySystemFormats[];
550
+ this.keyFormatPromise = this.getKeyFormatPromise(keyFormats);
551
+ }
542
552
 
543
- if (
544
- initDataType === 'sinf' &&
545
- this.config.drmSystems[KeySystems.FAIRPLAY]
546
- ) {
547
- // Match sinf keyId to playlist skd://keyId=
548
- const json = bin2str(new Uint8Array(initData));
549
- try {
550
- const sinf = base64Decode(JSON.parse(json).sinf);
551
- const tenc = parseSinf(new Uint8Array(sinf));
552
- if (!tenc) {
553
- throw new Error(
554
- `'schm' box missing or not cbcs/cenc with schi > tenc`,
553
+ this.keyFormatPromise.then((keySystemFormat) => {
554
+ const keySystem = keySystemFormatToKeySystemDomain(keySystemFormat);
555
+
556
+ let keyId: Uint8Array | null | undefined;
557
+ let keySystemDomain: KeySystems | undefined;
558
+
559
+ if (initDataType === 'sinf') {
560
+ if (keySystem !== KeySystems.FAIRPLAY) {
561
+ this.warn(
562
+ `Ignoring unexpected "${event.type}" event with init data type: "${initDataType}" for selected key-system ${keySystem}`,
555
563
  );
564
+ return;
556
565
  }
557
- keyId = tenc.subarray(8, 24);
558
- keySystemDomain = KeySystems.FAIRPLAY;
559
- } catch (error) {
560
- this.warn(`${logMessage} Failed to parse sinf: ${error}`);
561
- return;
562
- }
563
- } else {
564
- // Support Widevine clear-lead key-session creation (otherwise depend on playlist keys)
565
- const psshResults = parseMultiPssh(initData);
566
- const psshInfo = psshResults.filter(
567
- (pssh): pssh is PsshData => pssh.systemId === KeySystemIds.WIDEVINE,
568
- )[0];
569
- if (!psshInfo) {
566
+ // Match sinf keyId to playlist skd://keyId=
567
+ const json = bin2str(new Uint8Array(initData));
568
+ try {
569
+ const sinf = base64Decode(JSON.parse(json).sinf);
570
+ const tenc = parseSinf(sinf);
571
+ if (!tenc) {
572
+ throw new Error(
573
+ `'schm' box missing or not cbcs/cenc with schi > tenc`,
574
+ );
575
+ }
576
+ keyId = tenc.subarray(8, 24);
577
+ keySystemDomain = KeySystems.FAIRPLAY;
578
+ } catch (error) {
579
+ this.warn(`${logMessage} Failed to parse sinf: ${error}`);
580
+ return;
581
+ }
582
+ } else {
570
583
  if (
571
- psshResults.length === 0 ||
572
- psshResults.some((pssh): pssh is PsshInvalidResult => !pssh.systemId)
584
+ keySystem !== KeySystems.WIDEVINE &&
585
+ keySystem !== KeySystems.PLAYREADY
573
586
  ) {
574
- this.warn(`${logMessage} contains incomplete or invalid pssh data`);
575
- } else {
576
- this.log(
577
- `ignoring ${logMessage} for ${(psshResults as PsshData[])
578
- .map((pssh) => keySystemIdToKeySystemDomain(pssh.systemId))
579
- .join(',')} pssh data in favor of playlist keys`,
587
+ this.warn(
588
+ `Ignoring unexpected "${event.type}" event with init data type: "${initDataType}" for selected key-system ${keySystem}`,
580
589
  );
590
+ return;
581
591
  }
582
- return;
583
- }
584
- keySystemDomain = keySystemIdToKeySystemDomain(psshInfo.systemId);
585
- if (psshInfo.version === 0 && psshInfo.data) {
586
- const offset = psshInfo.data.length - 22;
587
- keyId = psshInfo.data.subarray(offset, offset + 16);
588
- }
589
- }
590
-
591
- if (!keySystemDomain || !keyId) {
592
- return;
593
- }
592
+ // Support Widevine/PlayReady clear-lead key-session creation (otherwise depend on playlist keys)
593
+ const psshResults = parseMultiPssh(initData);
594
594
 
595
- const keyIdHex = Hex.hexDump(keyId);
596
- const { keyIdToKeySessionPromise, mediaKeySessions } = this;
595
+ const psshInfos = psshResults.filter(
596
+ (pssh): pssh is PsshData =>
597
+ !!pssh.systemId &&
598
+ keySystemIdToKeySystemDomain(pssh.systemId) === keySystem,
599
+ );
597
600
 
598
- let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex];
599
- for (let i = 0; i < mediaKeySessions.length; i++) {
600
- // Match playlist key
601
- const keyContext = mediaKeySessions[i];
602
- const decryptdata = keyContext.decryptdata;
603
- if (!decryptdata.keyId) {
604
- continue;
605
- }
606
- const oldKeyIdHex = Hex.hexDump(decryptdata.keyId);
607
- if (
608
- keyIdHex === oldKeyIdHex ||
609
- decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1
610
- ) {
611
- keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex];
612
- if (decryptdata.pssh) {
613
- break;
601
+ if (psshInfos.length > 1) {
602
+ this.warn(
603
+ `${logMessage} Using first of ${psshInfos.length} pssh found for selected key-system ${keySystem}`,
604
+ );
614
605
  }
615
- delete keyIdToKeySessionPromise[oldKeyIdHex];
616
- decryptdata.pssh = new Uint8Array(initData);
617
- decryptdata.keyId = keyId;
618
- keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
619
- keySessionContextPromise.then(() => {
620
- return this.generateRequestWithPreferredKeySession(
621
- keyContext,
622
- initDataType,
623
- initData,
624
- 'encrypted-event-key-match',
606
+
607
+ const psshInfo = psshInfos[0];
608
+
609
+ if (!psshInfo) {
610
+ if (
611
+ psshResults.length === 0 ||
612
+ psshResults.some(
613
+ (pssh): pssh is PsshInvalidResult => !pssh.systemId,
614
+ )
615
+ ) {
616
+ this.warn(`${logMessage} contains incomplete or invalid pssh data`);
617
+ } else {
618
+ this.log(
619
+ `ignoring ${logMessage} for ${(psshResults as PsshData[])
620
+ .map((pssh) => keySystemIdToKeySystemDomain(pssh.systemId))
621
+ .join(',')} pssh data in favor of playlist keys`,
625
622
  );
626
- });
627
- break;
623
+ }
624
+ return;
625
+ }
626
+
627
+ keySystemDomain = keySystemIdToKeySystemDomain(psshInfo.systemId);
628
+ if (psshInfo.version === 0 && psshInfo.data) {
629
+ if (keySystemDomain === KeySystems.WIDEVINE) {
630
+ const offset = psshInfo.data.length - 22;
631
+ keyId = psshInfo.data.subarray(offset, offset + 16);
632
+ } else if (keySystemDomain === KeySystems.PLAYREADY) {
633
+ keyId = parsePlayReadyWRM(psshInfo.data);
634
+ }
635
+ }
628
636
  }
629
- }
630
637
 
631
- if (!keySessionContextPromise) {
632
- // Clear-lead key (not encountered in playlist)
633
- keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
634
- this.getKeySystemSelectionPromise([keySystemDomain]).then(
635
- ({ keySystem, mediaKeys }) => {
636
- this.throwIfDestroyed();
637
- const decryptdata = new LevelKey(
638
- 'ISO-23001-7',
639
- keyIdHex,
640
- keySystemToKeySystemFormat(keySystem) ?? '',
641
- );
642
- decryptdata.pssh = new Uint8Array(initData);
643
- decryptdata.keyId = keyId as Uint8Array;
644
- return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => {
645
- this.throwIfDestroyed();
646
- const keySessionContext = this.createMediaKeySessionContext({
647
- decryptdata,
648
- keySystem,
649
- mediaKeys,
650
- });
638
+ if (!keySystemDomain || !keyId) {
639
+ this.log(`Unable to handle ${logMessage} with key-system ${keySystem}`);
640
+ return;
641
+ }
642
+
643
+ const keyIdHex = Hex.hexDump(keyId);
644
+ const { keyIdToKeySessionPromise, mediaKeySessions } = this;
645
+
646
+ let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex];
647
+ for (let i = 0; i < mediaKeySessions.length; i++) {
648
+ // Match playlist key
649
+ const keyContext = mediaKeySessions[i];
650
+ const decryptdata = keyContext.decryptdata;
651
+ if (!decryptdata.keyId) {
652
+ continue;
653
+ }
654
+ const oldKeyIdHex = Hex.hexDump(decryptdata.keyId);
655
+ if (
656
+ keyIdHex === oldKeyIdHex ||
657
+ decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1
658
+ ) {
659
+ keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex];
660
+ if (decryptdata.pssh) {
661
+ break;
662
+ }
663
+ delete keyIdToKeySessionPromise[oldKeyIdHex];
664
+ decryptdata.pssh = new Uint8Array(initData);
665
+ decryptdata.keyId = keyId;
666
+ keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
667
+ keySessionContextPromise.then(() => {
651
668
  return this.generateRequestWithPreferredKeySession(
652
- keySessionContext,
669
+ keyContext,
653
670
  initDataType,
654
671
  initData,
655
- 'encrypted-event-no-match',
672
+ 'encrypted-event-key-match',
656
673
  );
657
674
  });
658
- },
659
- );
660
- }
661
- keySessionContextPromise.catch((error) => this.handleError(error));
662
- }
675
+ keySessionContextPromise.catch((error) => this.handleError(error));
676
+ break;
677
+ }
678
+ }
679
+
680
+ if (!keySessionContextPromise) {
681
+ if (keySystemDomain !== keySystem) {
682
+ this.log(
683
+ `Ignoring "${logMessage}" with ${keySystemDomain} init data for selected key-system ${keySystem}`,
684
+ );
685
+ return;
686
+ }
687
+ // "Clear-lead" (misc key not encountered in playlist)
688
+ keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
689
+ this.getKeySystemSelectionPromise([keySystemDomain]).then(
690
+ ({ keySystem, mediaKeys }) => {
691
+ this.throwIfDestroyed();
692
+
693
+ const decryptdata = new LevelKey(
694
+ 'ISO-23001-7',
695
+ keyIdHex,
696
+ keySystemToKeySystemFormat(keySystem) ?? '',
697
+ );
698
+ decryptdata.pssh = new Uint8Array(initData);
699
+ decryptdata.keyId = keyId as Uint8Array;
700
+ return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => {
701
+ this.throwIfDestroyed();
702
+ const keySessionContext = this.createMediaKeySessionContext({
703
+ decryptdata,
704
+ keySystem,
705
+ mediaKeys,
706
+ });
707
+ return this.generateRequestWithPreferredKeySession(
708
+ keySessionContext,
709
+ initDataType,
710
+ initData,
711
+ 'encrypted-event-no-match',
712
+ );
713
+ });
714
+ },
715
+ );
663
716
 
664
- private _onWaitingForKey(event: Event) {
717
+ keySessionContextPromise.catch((error) => this.handleError(error));
718
+ }
719
+ });
720
+ };
721
+
722
+ private onWaitingForKey = (event: Event) => {
665
723
  this.log(`"${event.type}" event`);
666
- }
724
+ };
667
725
 
668
726
  private attemptSetMediaKeys(
669
727
  keySystem: KeySystems,
@@ -1111,7 +1169,7 @@ class EMEController implements ComponentAPI {
1111
1169
  ): Promise<ArrayBuffer> {
1112
1170
  const keyLoadPolicy = this.config.keyLoadPolicy.default;
1113
1171
  return new Promise((resolve, reject) => {
1114
- const url = this.getLicenseServerUrl(keySessionContext.keySystem);
1172
+ const url = this.getLicenseServerUrlOrThrow(keySessionContext.keySystem);
1115
1173
  this.log(`Sending license request to URL: ${url}`);
1116
1174
  const xhr = new XMLHttpRequest();
1117
1175
  xhr.responseType = 'arraybuffer';
@@ -1216,6 +1274,8 @@ class EMEController implements ComponentAPI {
1216
1274
  // keep reference of media
1217
1275
  this.media = media;
1218
1276
 
1277
+ media.removeEventListener('encrypted', this.onMediaEncrypted);
1278
+ media.removeEventListener('waitingforkey', this.onWaitingForKey);
1219
1279
  media.addEventListener('encrypted', this.onMediaEncrypted);
1220
1280
  media.addEventListener('waitingforkey', this.onWaitingForKey);
1221
1281
  }
@@ -237,7 +237,7 @@ export default class StreamController
237
237
  return;
238
238
  }
239
239
 
240
- const level = hls.nextLoadLevel;
240
+ const level = this.buffering ? hls.nextLoadLevel : hls.loadLevel;
241
241
  if (!levels?.[level]) {
242
242
  return;
243
243
  }
@@ -262,6 +262,9 @@ export default class StreamController
262
262
  this.state = State.ENDED;
263
263
  return;
264
264
  }
265
+ if (!this.buffering) {
266
+ return;
267
+ }
265
268
 
266
269
  // set next load level : this will trigger a playlist load if needed
267
270
  if (hls.loadLevel !== level && hls.manualLevel === -1) {
package/src/hls.ts CHANGED
@@ -430,9 +430,13 @@ export default class Hls implements HlsEventEmitter {
430
430
  startLoad(startPosition: number = -1) {
431
431
  logger.log(`startLoad(${startPosition})`);
432
432
  this.started = true;
433
- this.networkControllers.forEach((controller) => {
434
- controller.startLoad(startPosition);
435
- });
433
+ this.resumeBuffering();
434
+ for (let i = 0; i < this.networkControllers.length; i++) {
435
+ this.networkControllers[i].startLoad(startPosition);
436
+ if (!this.started || !this.networkControllers) {
437
+ break;
438
+ }
439
+ }
436
440
  }
437
441
 
438
442
  /**
@@ -441,32 +445,35 @@ export default class Hls implements HlsEventEmitter {
441
445
  stopLoad() {
442
446
  logger.log('stopLoad');
443
447
  this.started = false;
444
- this.networkControllers.forEach((controller) => {
445
- controller.stopLoad();
446
- });
448
+ for (let i = 0; i < this.networkControllers.length; i++) {
449
+ this.networkControllers[i].stopLoad();
450
+ if (this.started || !this.networkControllers) {
451
+ break;
452
+ }
453
+ }
447
454
  }
448
455
 
449
456
  /**
450
- * Resumes stream controller segment loading if previously started.
457
+ * Resumes stream controller segment loading after `pauseBuffering` has been called.
451
458
  */
452
459
  resumeBuffering() {
453
- if (this.started) {
454
- this.networkControllers.forEach((controller) => {
455
- if ('fragmentLoader' in controller) {
456
- controller.startLoad(-1);
457
- }
458
- });
459
- }
460
+ logger.log(`resume buffering`);
461
+ this.networkControllers.forEach((controller) => {
462
+ if (controller.resumeBuffering) {
463
+ controller.resumeBuffering();
464
+ }
465
+ });
460
466
  }
461
467
 
462
468
  /**
463
- * Stops stream controller segment loading without changing 'started' state like stopLoad().
469
+ * Prevents stream controller from loading new segments until `resumeBuffering` is called.
464
470
  * This allows for media buffering to be paused without interupting playlist loading.
465
471
  */
466
472
  pauseBuffering() {
473
+ logger.log(`pause buffering`);
467
474
  this.networkControllers.forEach((controller) => {
468
- if ('fragmentLoader' in controller) {
469
- controller.stopLoad();
475
+ if (controller.pauseBuffering) {
476
+ controller.pauseBuffering();
470
477
  }
471
478
  });
472
479
  }
@@ -112,7 +112,12 @@ export default class KeyLoader implements ComponentAPI {
112
112
  }
113
113
 
114
114
  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
+ ) {
116
121
  // Multiple keys, but none selected, resolve in eme-controller
117
122
  return this.emeController
118
123
  .selectKeySystemFormat(frag)
@@ -2,7 +2,7 @@ import {
2
2
  changeEndianness,
3
3
  convertDataUriToArrayBytes,
4
4
  } from '../utils/keysystem-util';
5
- import { KeySystemFormats } from '../utils/mediakeys-helper';
5
+ import { KeySystemFormats, parsePlayReadyWRM } from '../utils/mediakeys-helper';
6
6
  import { mp4pssh } from '../utils/mp4-tools';
7
7
  import { logger } from '../utils/logger';
8
8
  import { base64Decode } from '../utils/numeric-encoding-utils';
@@ -121,6 +121,8 @@ export class LevelKey implements DecryptData {
121
121
  if (keyBytes) {
122
122
  switch (this.keyFormat) {
123
123
  case KeySystemFormats.WIDEVINE:
124
+ // Setting `pssh` on this LevelKey/DecryptData allows HLS.js to generate a session using
125
+ // the playlist-key before the "encrypted" event. (Comment out to only use "encrypted" path.)
124
126
  this.pssh = keyBytes;
125
127
  // In case of widevine keyID is embedded in PSSH box. Read Key ID.
126
128
  if (keyBytes.length >= 22) {
@@ -136,38 +138,12 @@ export class LevelKey implements DecryptData {
136
138
  0x5b, 0xe0, 0x88, 0x5f, 0x95,
137
139
  ]);
138
140
 
141
+ // Setting `pssh` on this LevelKey/DecryptData allows HLS.js to generate a session using
142
+ // the playlist-key before the "encrypted" event. (Comment out to only use "encrypted" path.)
139
143
  this.pssh = mp4pssh(PlayReadyKeySystemUUID, null, keyBytes);
140
144
 
141
- const keyBytesUtf16 = new Uint16Array(
142
- keyBytes.buffer,
143
- keyBytes.byteOffset,
144
- keyBytes.byteLength / 2,
145
- );
146
- const keyByteStr = String.fromCharCode.apply(
147
- null,
148
- Array.from(keyBytesUtf16),
149
- );
145
+ this.keyId = parsePlayReadyWRM(keyBytes);
150
146
 
151
- // Parse Playready WRMHeader XML
152
- const xmlKeyBytes = keyByteStr.substring(
153
- keyByteStr.indexOf('<'),
154
- keyByteStr.length,
155
- );
156
- const parser = new DOMParser();
157
- const xmlDoc = parser.parseFromString(xmlKeyBytes, 'text/xml');
158
- const keyData = xmlDoc.getElementsByTagName('KID')[0];
159
- if (keyData) {
160
- const keyId = keyData.childNodes[0]
161
- ? keyData.childNodes[0].nodeValue
162
- : keyData.getAttribute('VALUE');
163
- if (keyId) {
164
- const keyIdArray = base64Decode(keyId).subarray(0, 16);
165
- // KID value in PRO is a base64-encoded little endian GUID interpretation of UUID
166
- // KID value in ‘tenc’ is a big endian UUID GUID interpretation of UUID
167
- changeEndianness(keyIdArray);
168
- this.keyId = keyIdArray;
169
- }
170
- }
171
147
  break;
172
148
  }
173
149
  default: {
@@ -100,20 +100,24 @@ export default class MP4Remuxer implements Remuxer {
100
100
  this.videoTrackConfig = undefined;
101
101
  }
102
102
 
103
- getVideoStartPts(videoSamples) {
103
+ getVideoStartPts(videoSamples: VideoSample[]) {
104
+ // Get the minimum PTS value relative to the first sample's PTS, normalized for 33-bit wrapping
104
105
  let rolloverDetected = false;
106
+ const firstPts = videoSamples[0].pts;
105
107
  const startPTS = videoSamples.reduce((minPTS, sample) => {
106
- const delta = sample.pts - minPTS;
108
+ let pts = sample.pts;
109
+ let delta = pts - minPTS;
107
110
  if (delta < -4294967296) {
108
111
  // 2^32, see PTSNormalize for reasoning, but we're hitting a rollover here, and we don't want that to impact the timeOffset calculation
109
112
  rolloverDetected = true;
110
- return normalizePts(minPTS, sample.pts);
111
- } else if (delta > 0) {
113
+ pts = normalizePts(pts, firstPts);
114
+ delta = pts - minPTS;
115
+ }
116
+ if (delta > 0) {
112
117
  return minPTS;
113
- } else {
114
- return sample.pts;
115
118
  }
116
- }, videoSamples[0].pts);
119
+ return pts;
120
+ }, firstPts);
117
121
  if (rolloverDetected) {
118
122
  logger.debug('PTS rollover detected');
119
123
  }
@@ -15,4 +15,6 @@ export interface AbrComponentAPI extends ComponentAPI {
15
15
  export interface NetworkComponentAPI extends ComponentAPI {
16
16
  startLoad(startPosition: number): void;
17
17
  stopLoad(): void;
18
+ pauseBuffering?(): void;
19
+ resumeBuffering?(): void;
18
20
  }