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.
- package/dist/hls.js +250 -203
- package/dist/hls.js.d.ts +3 -4
- package/dist/hls.js.map +1 -1
- package/dist/hls.light.js +24 -29
- package/dist/hls.light.js.map +1 -1
- package/dist/hls.light.min.js +1 -1
- package/dist/hls.light.min.js.map +1 -1
- package/dist/hls.light.mjs +23 -28
- package/dist/hls.light.mjs.map +1 -1
- package/dist/hls.min.js +1 -1
- package/dist/hls.min.js.map +1 -1
- package/dist/hls.mjs +200 -157
- package/dist/hls.mjs.map +1 -1
- package/dist/hls.worker.js +1 -1
- package/package.json +1 -1
- package/src/controller/eme-controller.ts +184 -124
- package/src/loader/key-loader.ts +6 -1
- package/src/loader/level-key.ts +6 -30
- package/src/utils/level-helper.ts +37 -38
- package/src/utils/mediakeys-helper.ts +34 -1
@@ -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.
|
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 |
|
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
|
-
|
157
|
-
|
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
|
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
|
-
|
541
|
-
|
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
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
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
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
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
|
-
|
572
|
-
|
584
|
+
keySystem !== KeySystems.WIDEVINE &&
|
585
|
+
keySystem !== KeySystems.PLAYREADY
|
573
586
|
) {
|
574
|
-
this.warn(
|
575
|
-
|
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
|
-
|
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
|
-
|
596
|
-
|
595
|
+
const psshInfos = psshResults.filter(
|
596
|
+
(pssh): pssh is PsshData =>
|
597
|
+
!!pssh.systemId &&
|
598
|
+
keySystemIdToKeySystemDomain(pssh.systemId) === keySystem,
|
599
|
+
);
|
597
600
|
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
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
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
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
|
-
|
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
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
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
|
-
|
669
|
+
keyContext,
|
653
670
|
initDataType,
|
654
671
|
initData,
|
655
|
-
'encrypted-event-
|
672
|
+
'encrypted-event-key-match',
|
656
673
|
);
|
657
674
|
});
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
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
|
-
|
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.
|
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
|
}
|
package/src/loader/key-loader.ts
CHANGED
@@ -112,7 +112,12 @@ export default class KeyLoader implements ComponentAPI {
|
|
112
112
|
}
|
113
113
|
|
114
114
|
load(frag: Fragment): Promise<KeyLoadedData> {
|
115
|
-
if (
|
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)
|
package/src/loader/level-key.ts
CHANGED
@@ -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
|
-
|
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 = (
|
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
|
156
|
-
let
|
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
|
162
|
-
if (
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
-
?
|
202
|
-
:
|
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 =
|
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
|
-
|
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
|
+
}
|