hls.js 1.5.18 → 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.
@@ -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
  }
@@ -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: {
@@ -8,7 +8,12 @@ import { LevelDetails } from '../loader/level-details';
8
8
  import type { Level } from '../types/level';
9
9
  import { DateRange } from '../loader/date-range';
10
10
 
11
- type FragmentIntersection = (oldFrag: Fragment, newFrag: Fragment) => void;
11
+ type FragmentIntersection = (
12
+ oldFrag: Fragment,
13
+ newFrag: Fragment,
14
+ newFragIndex: number,
15
+ newFragments: Fragment[],
16
+ ) => void;
12
17
  type PartIntersection = (oldPart: Part, newPart: Part) => void;
13
18
 
14
19
  export function updatePTS(
@@ -106,7 +111,7 @@ export function updateFragPTSDTS(
106
111
  if (!details || sn < details.startSN || sn > details.endSN) {
107
112
  return 0;
108
113
  }
109
- let i;
114
+ let i: number;
110
115
  const fragIdx = sn - details.startSN;
111
116
  const fragments = details.fragments;
112
117
  // update frag reference in fragments array
@@ -152,18 +157,19 @@ export function mergeDetails(
152
157
  delete oldDetails.fragmentHint.endPTS;
153
158
  }
154
159
  // check if old/new playlists have fragments in common
155
- // loop through overlapping SN and update startPTS , cc, and duration if any found
156
- let ccOffset = 0;
157
- let PTSFrag;
160
+ // loop through overlapping SN and update startPTS, cc, and duration if any found
161
+ let PTSFrag: Fragment | undefined;
158
162
  mapFragmentIntersection(
159
163
  oldDetails,
160
164
  newDetails,
161
- (oldFrag: Fragment, newFrag: Fragment) => {
162
- if (oldFrag.relurl) {
163
- // Do not compare CC if the old fragment has no url. This is a level.fragmentHint used by LL-HLS parts.
164
- // It maybe be off by 1 if it was created before any parts or discontinuity tags were appended to the end
165
- // of the playlist.
166
- ccOffset = oldFrag.cc - newFrag.cc;
165
+ (oldFrag, newFrag, newFragIndex, newFragments) => {
166
+ if (newDetails.skippedSegments) {
167
+ if (newFrag.cc !== oldFrag.cc) {
168
+ const ccOffset = oldFrag.cc - newFrag.cc;
169
+ for (let i = newFragIndex; i < newFragments.length; i++) {
170
+ newFragments[i].cc += ccOffset;
171
+ }
172
+ }
167
173
  }
168
174
  if (
169
175
  Number.isFinite(oldFrag.startPTS) &&
@@ -196,10 +202,11 @@ export function mergeDetails(
196
202
  },
197
203
  );
198
204
 
205
+ const newFragments = newDetails.fragments;
199
206
  if (currentInitSegment) {
200
207
  const fragmentsToCheck = newDetails.fragmentHint
201
- ? newDetails.fragments.concat(newDetails.fragmentHint)
202
- : newDetails.fragments;
208
+ ? newFragments.concat(newDetails.fragmentHint)
209
+ : newFragments;
203
210
  fragmentsToCheck.forEach((frag) => {
204
211
  if (
205
212
  frag &&
@@ -212,34 +219,26 @@ export function mergeDetails(
212
219
  }
213
220
 
214
221
  if (newDetails.skippedSegments) {
215
- newDetails.deltaUpdateFailed = newDetails.fragments.some((frag) => !frag);
222
+ newDetails.deltaUpdateFailed = newFragments.some((frag) => !frag);
216
223
  if (newDetails.deltaUpdateFailed) {
217
224
  logger.warn(
218
225
  '[level-helper] Previous playlist missing segments skipped in delta playlist',
219
226
  );
220
227
  for (let i = newDetails.skippedSegments; i--; ) {
221
- newDetails.fragments.shift();
228
+ newFragments.shift();
229
+ }
230
+ newDetails.startSN = newFragments[0].sn as number;
231
+ } else {
232
+ if (newDetails.canSkipDateRanges) {
233
+ newDetails.dateRanges = mergeDateRanges(
234
+ oldDetails.dateRanges,
235
+ newDetails.dateRanges,
236
+ newDetails.recentlyRemovedDateranges,
237
+ );
222
238
  }
223
- newDetails.startSN = newDetails.fragments[0].sn as number;
224
- newDetails.startCC = newDetails.fragments[0].cc;
225
- } else if (newDetails.canSkipDateRanges) {
226
- newDetails.dateRanges = mergeDateRanges(
227
- oldDetails.dateRanges,
228
- newDetails.dateRanges,
229
- newDetails.recentlyRemovedDateranges,
230
- );
231
- }
232
- }
233
-
234
- const newFragments = newDetails.fragments;
235
- if (ccOffset) {
236
- logger.warn('discontinuity sliding from playlist, take drift into account');
237
- for (let i = 0; i < newFragments.length; i++) {
238
- newFragments[i].cc += ccOffset;
239
239
  }
240
- }
241
- if (newDetails.skippedSegments) {
242
240
  newDetails.startCC = newDetails.fragments[0].cc;
241
+ newDetails.endCC = newFragments[newFragments.length - 1].cc;
243
242
  }
244
243
 
245
244
  // Merge parts
@@ -257,10 +256,10 @@ export function mergeDetails(
257
256
  updateFragPTSDTS(
258
257
  newDetails,
259
258
  PTSFrag,
260
- PTSFrag.startPTS,
261
- PTSFrag.endPTS,
262
- PTSFrag.startDTS,
263
- PTSFrag.endDTS,
259
+ PTSFrag.startPTS as number,
260
+ PTSFrag.endPTS as number,
261
+ PTSFrag.startDTS as number,
262
+ PTSFrag.endDTS as number,
264
263
  );
265
264
  } else {
266
265
  // ensure that delta is within oldFragments range
@@ -371,7 +370,7 @@ export function mapFragmentIntersection(
371
370
  newFrag = newDetails.fragments[i] = oldFrag;
372
371
  }
373
372
  if (oldFrag && newFrag) {
374
- intersectionFn(oldFrag, newFrag);
373
+ intersectionFn(oldFrag, newFrag, i, newFrags);
375
374
  }
376
375
  }
377
376
  }
@@ -1,5 +1,7 @@
1
- import type { DRMSystemOptions, EMEControllerConfig } from '../config';
2
1
  import { optionalSelf } from './global';
2
+ import { changeEndianness } from './keysystem-util';
3
+ import { base64Decode } from './numeric-encoding-utils';
4
+ import type { DRMSystemOptions, EMEControllerConfig } from '../config';
3
5
 
4
6
  /**
5
7
  * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/requestMediaKeySystemAccess
@@ -163,3 +165,34 @@ function createMediaKeySystemConfigurations(
163
165
 
164
166
  return [baseConfig];
165
167
  }
168
+
169
+ export function parsePlayReadyWRM(keyBytes: Uint8Array): Uint8Array | null {
170
+ const keyBytesUtf16 = new Uint16Array(
171
+ keyBytes.buffer,
172
+ keyBytes.byteOffset,
173
+ keyBytes.byteLength / 2,
174
+ );
175
+ const keyByteStr = String.fromCharCode.apply(null, Array.from(keyBytesUtf16));
176
+
177
+ // Parse Playready WRMHeader XML
178
+ const xmlKeyBytes = keyByteStr.substring(
179
+ keyByteStr.indexOf('<'),
180
+ keyByteStr.length,
181
+ );
182
+ const parser = new DOMParser();
183
+ const xmlDoc = parser.parseFromString(xmlKeyBytes, 'text/xml');
184
+ const keyData = xmlDoc.getElementsByTagName('KID')[0];
185
+ if (keyData) {
186
+ const keyId = keyData.childNodes[0]
187
+ ? keyData.childNodes[0].nodeValue
188
+ : keyData.getAttribute('VALUE');
189
+ if (keyId) {
190
+ const keyIdArray = base64Decode(keyId).subarray(0, 16);
191
+ // KID value in PRO is a base64-encoded little endian GUID interpretation of UUID
192
+ // KID value in ‘tenc’ is a big endian UUID GUID interpretation of UUID
193
+ changeEndianness(keyIdArray);
194
+ return keyIdArray;
195
+ }
196
+ }
197
+ return null;
198
+ }