@zenvor/hls.js 1.0.0
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/LICENSE +28 -0
- package/README.md +472 -0
- package/dist/hls-demo.js +26995 -0
- package/dist/hls-demo.js.map +1 -0
- package/dist/hls.d.mts +4204 -0
- package/dist/hls.d.ts +4204 -0
- package/dist/hls.js +40050 -0
- package/dist/hls.js.d.ts +4204 -0
- package/dist/hls.js.map +1 -0
- package/dist/hls.light.js +27145 -0
- package/dist/hls.light.js.map +1 -0
- package/dist/hls.light.min.js +2 -0
- package/dist/hls.light.min.js.map +1 -0
- package/dist/hls.light.mjs +26392 -0
- package/dist/hls.light.mjs.map +1 -0
- package/dist/hls.min.js +2 -0
- package/dist/hls.min.js.map +1 -0
- package/dist/hls.mjs +38956 -0
- package/dist/hls.mjs.map +1 -0
- package/dist/hls.worker.js +2 -0
- package/dist/hls.worker.js.map +1 -0
- package/package.json +143 -0
- package/src/config.ts +794 -0
- package/src/controller/abr-controller.ts +1019 -0
- package/src/controller/algo-data-controller.ts +794 -0
- package/src/controller/audio-stream-controller.ts +1099 -0
- package/src/controller/audio-track-controller.ts +454 -0
- package/src/controller/base-playlist-controller.ts +438 -0
- package/src/controller/base-stream-controller.ts +2526 -0
- package/src/controller/buffer-controller.ts +2015 -0
- package/src/controller/buffer-operation-queue.ts +159 -0
- package/src/controller/cap-level-controller.ts +367 -0
- package/src/controller/cmcd-controller.ts +422 -0
- package/src/controller/content-steering-controller.ts +622 -0
- package/src/controller/eme-controller.ts +1617 -0
- package/src/controller/error-controller.ts +627 -0
- package/src/controller/fps-controller.ts +146 -0
- package/src/controller/fragment-finders.ts +256 -0
- package/src/controller/fragment-tracker.ts +567 -0
- package/src/controller/gap-controller.ts +719 -0
- package/src/controller/id3-track-controller.ts +488 -0
- package/src/controller/interstitial-player.ts +302 -0
- package/src/controller/interstitials-controller.ts +2895 -0
- package/src/controller/interstitials-schedule.ts +698 -0
- package/src/controller/latency-controller.ts +294 -0
- package/src/controller/level-controller.ts +776 -0
- package/src/controller/stream-controller.ts +1597 -0
- package/src/controller/subtitle-stream-controller.ts +508 -0
- package/src/controller/subtitle-track-controller.ts +617 -0
- package/src/controller/timeline-controller.ts +677 -0
- package/src/crypt/aes-crypto.ts +36 -0
- package/src/crypt/aes-decryptor.ts +339 -0
- package/src/crypt/decrypter-aes-mode.ts +4 -0
- package/src/crypt/decrypter.ts +225 -0
- package/src/crypt/fast-aes-key.ts +39 -0
- package/src/define-plugin.d.ts +17 -0
- package/src/demux/audio/aacdemuxer.ts +126 -0
- package/src/demux/audio/ac3-demuxer.ts +170 -0
- package/src/demux/audio/adts.ts +249 -0
- package/src/demux/audio/base-audio-demuxer.ts +205 -0
- package/src/demux/audio/dolby.ts +21 -0
- package/src/demux/audio/mp3demuxer.ts +85 -0
- package/src/demux/audio/mpegaudio.ts +177 -0
- package/src/demux/chunk-cache.ts +42 -0
- package/src/demux/dummy-demuxed-track.ts +13 -0
- package/src/demux/inject-worker.ts +75 -0
- package/src/demux/mp4demuxer.ts +234 -0
- package/src/demux/sample-aes.ts +198 -0
- package/src/demux/transmuxer-interface.ts +449 -0
- package/src/demux/transmuxer-worker.ts +221 -0
- package/src/demux/transmuxer.ts +560 -0
- package/src/demux/tsdemuxer.ts +1256 -0
- package/src/demux/video/avc-video-parser.ts +401 -0
- package/src/demux/video/base-video-parser.ts +198 -0
- package/src/demux/video/exp-golomb.ts +153 -0
- package/src/demux/video/hevc-video-parser.ts +736 -0
- package/src/empty-es.js +5 -0
- package/src/empty.js +3 -0
- package/src/errors.ts +107 -0
- package/src/events.ts +548 -0
- package/src/exports-default.ts +3 -0
- package/src/exports-named.ts +81 -0
- package/src/hls.ts +1613 -0
- package/src/is-supported.ts +54 -0
- package/src/loader/date-range.ts +207 -0
- package/src/loader/fragment-loader.ts +403 -0
- package/src/loader/fragment.ts +487 -0
- package/src/loader/interstitial-asset-list.ts +162 -0
- package/src/loader/interstitial-event.ts +337 -0
- package/src/loader/key-loader.ts +439 -0
- package/src/loader/level-details.ts +203 -0
- package/src/loader/level-key.ts +259 -0
- package/src/loader/load-stats.ts +17 -0
- package/src/loader/m3u8-parser.ts +1072 -0
- package/src/loader/playlist-loader.ts +839 -0
- package/src/polyfills/number.ts +15 -0
- package/src/remux/aac-helper.ts +81 -0
- package/src/remux/mp4-generator.ts +1380 -0
- package/src/remux/mp4-remuxer.ts +1261 -0
- package/src/remux/passthrough-remuxer.ts +434 -0
- package/src/task-loop.ts +130 -0
- package/src/types/algo.ts +44 -0
- package/src/types/buffer.ts +105 -0
- package/src/types/component-api.ts +20 -0
- package/src/types/demuxer.ts +208 -0
- package/src/types/events.ts +574 -0
- package/src/types/fragment-tracker.ts +23 -0
- package/src/types/level.ts +268 -0
- package/src/types/loader.ts +198 -0
- package/src/types/media-playlist.ts +92 -0
- package/src/types/network-details.ts +3 -0
- package/src/types/remuxer.ts +104 -0
- package/src/types/track.ts +12 -0
- package/src/types/transmuxer.ts +46 -0
- package/src/types/tuples.ts +6 -0
- package/src/types/vtt.ts +11 -0
- package/src/utils/arrays.ts +22 -0
- package/src/utils/attr-list.ts +192 -0
- package/src/utils/binary-search.ts +46 -0
- package/src/utils/buffer-helper.ts +173 -0
- package/src/utils/cea-608-parser.ts +1413 -0
- package/src/utils/chunker.ts +41 -0
- package/src/utils/codecs.ts +314 -0
- package/src/utils/cues.ts +96 -0
- package/src/utils/discontinuities.ts +174 -0
- package/src/utils/encryption-methods-util.ts +21 -0
- package/src/utils/error-helper.ts +95 -0
- package/src/utils/event-listener-helper.ts +16 -0
- package/src/utils/ewma-bandwidth-estimator.ts +97 -0
- package/src/utils/ewma.ts +43 -0
- package/src/utils/fetch-loader.ts +331 -0
- package/src/utils/global.ts +2 -0
- package/src/utils/hash.ts +10 -0
- package/src/utils/hdr.ts +67 -0
- package/src/utils/hex.ts +32 -0
- package/src/utils/imsc1-ttml-parser.ts +261 -0
- package/src/utils/keysystem-util.ts +45 -0
- package/src/utils/level-helper.ts +629 -0
- package/src/utils/logger.ts +120 -0
- package/src/utils/media-option-attributes.ts +49 -0
- package/src/utils/mediacapabilities-helper.ts +301 -0
- package/src/utils/mediakeys-helper.ts +210 -0
- package/src/utils/mediasource-helper.ts +37 -0
- package/src/utils/mp4-tools.ts +1473 -0
- package/src/utils/number.ts +3 -0
- package/src/utils/numeric-encoding-utils.ts +26 -0
- package/src/utils/output-filter.ts +46 -0
- package/src/utils/rendition-helper.ts +505 -0
- package/src/utils/safe-json-stringify.ts +22 -0
- package/src/utils/texttrack-utils.ts +164 -0
- package/src/utils/time-ranges.ts +17 -0
- package/src/utils/timescale-conversion.ts +46 -0
- package/src/utils/utf8-utils.ts +18 -0
- package/src/utils/variable-substitution.ts +105 -0
- package/src/utils/vttcue.ts +384 -0
- package/src/utils/vttparser.ts +497 -0
- package/src/utils/webvtt-parser.ts +166 -0
- package/src/utils/xhr-loader.ts +337 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,1617 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author Stephan Hesse <disparat@gmail.com> | <tchakabam@gmail.com>
|
|
3
|
+
*
|
|
4
|
+
* DRM support for Hls.js
|
|
5
|
+
*/
|
|
6
|
+
import { EventEmitter } from 'eventemitter3';
|
|
7
|
+
import { ErrorDetails, ErrorTypes } from '../errors';
|
|
8
|
+
import { Events } from '../events';
|
|
9
|
+
import { LevelKey } from '../loader/level-key';
|
|
10
|
+
import { arrayValuesMatch } from '../utils/arrays';
|
|
11
|
+
import {
|
|
12
|
+
addEventListener,
|
|
13
|
+
removeEventListener,
|
|
14
|
+
} from '../utils/event-listener-helper';
|
|
15
|
+
import { arrayToHex } from '../utils/hex';
|
|
16
|
+
import { changeEndianness } from '../utils/keysystem-util';
|
|
17
|
+
import { Logger } from '../utils/logger';
|
|
18
|
+
import {
|
|
19
|
+
getKeySystemsForConfig,
|
|
20
|
+
getSupportedMediaKeySystemConfigurations,
|
|
21
|
+
isPersistentSessionType,
|
|
22
|
+
keySystemDomainToKeySystemFormat,
|
|
23
|
+
keySystemFormatToKeySystemDomain,
|
|
24
|
+
KeySystems,
|
|
25
|
+
requestMediaKeySystemAccess,
|
|
26
|
+
} from '../utils/mediakeys-helper';
|
|
27
|
+
import { bin2str, parseSinf } from '../utils/mp4-tools';
|
|
28
|
+
import { base64Decode } from '../utils/numeric-encoding-utils';
|
|
29
|
+
import { stringify } from '../utils/safe-json-stringify';
|
|
30
|
+
import { strToUtf8array } from '../utils/utf8-utils';
|
|
31
|
+
import type { EMEControllerConfig, HlsConfig, LoadPolicy } from '../config';
|
|
32
|
+
import type Hls from '../hls';
|
|
33
|
+
import type { Fragment } from '../loader/fragment';
|
|
34
|
+
import type { DecryptData } from '../loader/level-key';
|
|
35
|
+
import type { ComponentAPI } from '../types/component-api';
|
|
36
|
+
import type {
|
|
37
|
+
ErrorData,
|
|
38
|
+
KeyLoadedData,
|
|
39
|
+
ManifestLoadedData,
|
|
40
|
+
MediaAttachedData,
|
|
41
|
+
} from '../types/events';
|
|
42
|
+
import type {
|
|
43
|
+
Loader,
|
|
44
|
+
LoaderCallbacks,
|
|
45
|
+
LoaderConfiguration,
|
|
46
|
+
LoaderContext,
|
|
47
|
+
} from '../types/loader';
|
|
48
|
+
import type { KeySystemFormats } from '../utils/mediakeys-helper';
|
|
49
|
+
|
|
50
|
+
interface KeySystemAccessPromises {
|
|
51
|
+
keySystemAccess: Promise<MediaKeySystemAccess>;
|
|
52
|
+
mediaKeys?: Promise<MediaKeys>;
|
|
53
|
+
certificate?: Promise<BufferSource | void>;
|
|
54
|
+
hasMediaKeys?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface MediaKeySessionContext {
|
|
58
|
+
keySystem: KeySystems;
|
|
59
|
+
mediaKeys: MediaKeys;
|
|
60
|
+
decryptdata: LevelKey;
|
|
61
|
+
mediaKeysSession: MediaKeySession;
|
|
62
|
+
keyStatus?: MediaKeyStatus;
|
|
63
|
+
keyStatusTimeouts?: { [keyId: string]: number };
|
|
64
|
+
licenseXhr?: XMLHttpRequest;
|
|
65
|
+
_onmessage?: (this: MediaKeySession, ev: MediaKeyMessageEvent) => any;
|
|
66
|
+
_onkeystatuseschange?: (this: MediaKeySession, ev: Event) => any;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Controller to deal with encrypted media extensions (EME)
|
|
71
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Encrypted_Media_Extensions_API
|
|
72
|
+
*
|
|
73
|
+
* @class
|
|
74
|
+
* @constructor
|
|
75
|
+
*/
|
|
76
|
+
class EMEController extends Logger implements ComponentAPI {
|
|
77
|
+
public static CDMCleanupPromise: Promise<void> | void;
|
|
78
|
+
|
|
79
|
+
private readonly hls: Hls;
|
|
80
|
+
private readonly config: EMEControllerConfig & {
|
|
81
|
+
loader: { new (confg: HlsConfig): Loader<LoaderContext> };
|
|
82
|
+
certLoadPolicy: LoadPolicy;
|
|
83
|
+
keyLoadPolicy: LoadPolicy;
|
|
84
|
+
};
|
|
85
|
+
private media: HTMLMediaElement | null = null;
|
|
86
|
+
private mediaResolved?: () => void;
|
|
87
|
+
private keyFormatPromise: Promise<KeySystemFormats> | null = null;
|
|
88
|
+
private keySystemAccessPromises: {
|
|
89
|
+
[keysystem: string]: KeySystemAccessPromises | undefined;
|
|
90
|
+
} = {};
|
|
91
|
+
private _requestLicenseFailureCount: number = 0;
|
|
92
|
+
private mediaKeySessions: MediaKeySessionContext[] = [];
|
|
93
|
+
private keyIdToKeySessionPromise: {
|
|
94
|
+
[keyId: string]: Promise<MediaKeySessionContext> | undefined;
|
|
95
|
+
} = {};
|
|
96
|
+
private mediaKeys: MediaKeys | null = null;
|
|
97
|
+
private setMediaKeysQueue: Promise<void>[] = EMEController.CDMCleanupPromise
|
|
98
|
+
? [EMEController.CDMCleanupPromise]
|
|
99
|
+
: [];
|
|
100
|
+
private bannedKeyIds: { [keyId: string]: MediaKeyStatus | undefined } = {};
|
|
101
|
+
|
|
102
|
+
constructor(hls: Hls) {
|
|
103
|
+
super('eme', hls.logger);
|
|
104
|
+
this.hls = hls;
|
|
105
|
+
this.config = hls.config;
|
|
106
|
+
this.registerListeners();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public destroy() {
|
|
110
|
+
this.onDestroying();
|
|
111
|
+
this.onMediaDetached();
|
|
112
|
+
// Remove any references that could be held in config options or callbacks
|
|
113
|
+
const config = this.config;
|
|
114
|
+
config.requestMediaKeySystemAccessFunc = null;
|
|
115
|
+
config.licenseXhrSetup = config.licenseResponseCallback = undefined;
|
|
116
|
+
config.drmSystems = config.drmSystemOptions = {};
|
|
117
|
+
// @ts-ignore
|
|
118
|
+
this.hls = this.config = this.keyIdToKeySessionPromise = null;
|
|
119
|
+
// @ts-ignore
|
|
120
|
+
this.onMediaEncrypted = this.onWaitingForKey = null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private registerListeners() {
|
|
124
|
+
this.hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
|
|
125
|
+
this.hls.on(Events.MEDIA_DETACHED, this.onMediaDetached, this);
|
|
126
|
+
this.hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
|
|
127
|
+
this.hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
|
|
128
|
+
this.hls.on(Events.DESTROYING, this.onDestroying, this);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private unregisterListeners() {
|
|
132
|
+
this.hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
|
|
133
|
+
this.hls.off(Events.MEDIA_DETACHED, this.onMediaDetached, this);
|
|
134
|
+
this.hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
|
|
135
|
+
this.hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
|
|
136
|
+
this.hls.off(Events.DESTROYING, this.onDestroying, this);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private getLicenseServerUrl(keySystem: KeySystems): string | undefined {
|
|
140
|
+
const { drmSystems, widevineLicenseUrl } = this.config;
|
|
141
|
+
const keySystemConfiguration = drmSystems?.[keySystem];
|
|
142
|
+
|
|
143
|
+
if (keySystemConfiguration) {
|
|
144
|
+
return keySystemConfiguration.licenseUrl;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// For backward compatibility
|
|
148
|
+
if (keySystem === KeySystems.WIDEVINE && widevineLicenseUrl) {
|
|
149
|
+
return widevineLicenseUrl;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private getLicenseServerUrlOrThrow(keySystem: KeySystems): string | never {
|
|
154
|
+
const url = this.getLicenseServerUrl(keySystem);
|
|
155
|
+
if (url === undefined) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`no license server URL configured for key-system "${keySystem}"`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return url;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private getServerCertificateUrl(keySystem: KeySystems): string | void {
|
|
164
|
+
const { drmSystems } = this.config;
|
|
165
|
+
const keySystemConfiguration = drmSystems?.[keySystem];
|
|
166
|
+
|
|
167
|
+
if (keySystemConfiguration) {
|
|
168
|
+
return keySystemConfiguration.serverCertificateUrl;
|
|
169
|
+
} else {
|
|
170
|
+
this.log(`No Server Certificate in config.drmSystems["${keySystem}"]`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private attemptKeySystemAccess(
|
|
175
|
+
keySystemsToAttempt: KeySystems[],
|
|
176
|
+
): Promise<{ keySystem: KeySystems; mediaKeys: MediaKeys }> {
|
|
177
|
+
const levels = this.hls.levels;
|
|
178
|
+
const uniqueCodec = (value: string | undefined, i, a): value is string =>
|
|
179
|
+
!!value && a.indexOf(value) === i;
|
|
180
|
+
const audioCodecs = levels
|
|
181
|
+
.map((level) => level.audioCodec)
|
|
182
|
+
.filter(uniqueCodec);
|
|
183
|
+
const videoCodecs = levels
|
|
184
|
+
.map((level) => level.videoCodec)
|
|
185
|
+
.filter(uniqueCodec);
|
|
186
|
+
if (audioCodecs.length + videoCodecs.length === 0) {
|
|
187
|
+
videoCodecs.push('avc1.42e01e');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return new Promise(
|
|
191
|
+
(
|
|
192
|
+
resolve: (result: {
|
|
193
|
+
keySystem: KeySystems;
|
|
194
|
+
mediaKeys: MediaKeys;
|
|
195
|
+
}) => void,
|
|
196
|
+
reject: (Error) => void,
|
|
197
|
+
) => {
|
|
198
|
+
const attempt = (keySystems) => {
|
|
199
|
+
const keySystem = keySystems.shift();
|
|
200
|
+
this.getMediaKeysPromise(keySystem, audioCodecs, videoCodecs)
|
|
201
|
+
.then((mediaKeys) => resolve({ keySystem, mediaKeys }))
|
|
202
|
+
.catch((error) => {
|
|
203
|
+
if (keySystems.length) {
|
|
204
|
+
attempt(keySystems);
|
|
205
|
+
} else if (error instanceof EMEKeyError) {
|
|
206
|
+
reject(error);
|
|
207
|
+
} else {
|
|
208
|
+
reject(
|
|
209
|
+
new EMEKeyError(
|
|
210
|
+
{
|
|
211
|
+
type: ErrorTypes.KEY_SYSTEM_ERROR,
|
|
212
|
+
details: ErrorDetails.KEY_SYSTEM_NO_ACCESS,
|
|
213
|
+
error,
|
|
214
|
+
fatal: true,
|
|
215
|
+
},
|
|
216
|
+
error.message,
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
attempt(keySystemsToAttempt);
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private requestMediaKeySystemAccess(
|
|
228
|
+
keySystem: KeySystems,
|
|
229
|
+
supportedConfigurations: MediaKeySystemConfiguration[],
|
|
230
|
+
): Promise<MediaKeySystemAccess> {
|
|
231
|
+
const { requestMediaKeySystemAccessFunc } = this.config;
|
|
232
|
+
if (!(typeof requestMediaKeySystemAccessFunc === 'function')) {
|
|
233
|
+
let errMessage = `Configured requestMediaKeySystemAccess is not a function ${requestMediaKeySystemAccessFunc}`;
|
|
234
|
+
if (
|
|
235
|
+
requestMediaKeySystemAccess === null &&
|
|
236
|
+
self.location.protocol === 'http:'
|
|
237
|
+
) {
|
|
238
|
+
errMessage = `navigator.requestMediaKeySystemAccess is not available over insecure protocol ${location.protocol}`;
|
|
239
|
+
}
|
|
240
|
+
return Promise.reject(new Error(errMessage));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return requestMediaKeySystemAccessFunc(keySystem, supportedConfigurations);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private getMediaKeysPromise(
|
|
247
|
+
keySystem: KeySystems,
|
|
248
|
+
audioCodecs: string[],
|
|
249
|
+
videoCodecs: string[],
|
|
250
|
+
): Promise<MediaKeys> {
|
|
251
|
+
// This can throw, but is caught in event handler callpath
|
|
252
|
+
const mediaKeySystemConfigs = getSupportedMediaKeySystemConfigurations(
|
|
253
|
+
keySystem,
|
|
254
|
+
audioCodecs,
|
|
255
|
+
videoCodecs,
|
|
256
|
+
this.config.drmSystemOptions || {},
|
|
257
|
+
);
|
|
258
|
+
let keySystemAccessPromises = this.keySystemAccessPromises[keySystem];
|
|
259
|
+
let keySystemAccess = keySystemAccessPromises?.keySystemAccess;
|
|
260
|
+
if (!keySystemAccess) {
|
|
261
|
+
this.log(
|
|
262
|
+
`Requesting encrypted media "${keySystem}" key-system access with config: ${stringify(
|
|
263
|
+
mediaKeySystemConfigs,
|
|
264
|
+
)}`,
|
|
265
|
+
);
|
|
266
|
+
keySystemAccess = this.requestMediaKeySystemAccess(
|
|
267
|
+
keySystem,
|
|
268
|
+
mediaKeySystemConfigs,
|
|
269
|
+
);
|
|
270
|
+
const keySystemAccessPromisesNew = (keySystemAccessPromises =
|
|
271
|
+
this.keySystemAccessPromises[keySystem] =
|
|
272
|
+
{
|
|
273
|
+
keySystemAccess,
|
|
274
|
+
}) as KeySystemAccessPromises;
|
|
275
|
+
keySystemAccess.catch((error) => {
|
|
276
|
+
this.log(
|
|
277
|
+
`Failed to obtain access to key-system "${keySystem}": ${error}`,
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
return keySystemAccess.then((mediaKeySystemAccess) => {
|
|
281
|
+
this.log(
|
|
282
|
+
`Access for key-system "${mediaKeySystemAccess.keySystem}" obtained`,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const certificateRequest = this.fetchServerCertificate(keySystem);
|
|
286
|
+
|
|
287
|
+
this.log(`Create media-keys for "${keySystem}"`);
|
|
288
|
+
const mediaKeys = (keySystemAccessPromisesNew.mediaKeys =
|
|
289
|
+
mediaKeySystemAccess.createMediaKeys().then((mediaKeys) => {
|
|
290
|
+
this.log(`Media-keys created for "${keySystem}"`);
|
|
291
|
+
keySystemAccessPromisesNew.hasMediaKeys = true;
|
|
292
|
+
return certificateRequest.then((certificate) => {
|
|
293
|
+
if (certificate) {
|
|
294
|
+
return this.setMediaKeysServerCertificate(
|
|
295
|
+
mediaKeys,
|
|
296
|
+
keySystem,
|
|
297
|
+
certificate,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return mediaKeys;
|
|
301
|
+
});
|
|
302
|
+
}));
|
|
303
|
+
|
|
304
|
+
mediaKeys.catch((error) => {
|
|
305
|
+
this.error(
|
|
306
|
+
`Failed to create media-keys for "${keySystem}"}: ${error}`,
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
return mediaKeys;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return keySystemAccess.then(() => keySystemAccessPromises!.mediaKeys!);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private createMediaKeySessionContext({
|
|
317
|
+
decryptdata,
|
|
318
|
+
keySystem,
|
|
319
|
+
mediaKeys,
|
|
320
|
+
}: {
|
|
321
|
+
decryptdata: LevelKey;
|
|
322
|
+
keySystem: KeySystems;
|
|
323
|
+
mediaKeys: MediaKeys;
|
|
324
|
+
}): MediaKeySessionContext {
|
|
325
|
+
this.log(
|
|
326
|
+
`Creating key-system session "${keySystem}" keyId: ${arrayToHex(
|
|
327
|
+
decryptdata.keyId || ([] as number[]),
|
|
328
|
+
)} keyUri: ${decryptdata.uri}`,
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const mediaKeysSession = mediaKeys.createSession();
|
|
332
|
+
|
|
333
|
+
const mediaKeySessionContext: MediaKeySessionContext = {
|
|
334
|
+
decryptdata,
|
|
335
|
+
keySystem,
|
|
336
|
+
mediaKeys,
|
|
337
|
+
mediaKeysSession,
|
|
338
|
+
keyStatus: 'status-pending',
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
this.mediaKeySessions.push(mediaKeySessionContext);
|
|
342
|
+
|
|
343
|
+
return mediaKeySessionContext;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private renewKeySession(mediaKeySessionContext: MediaKeySessionContext) {
|
|
347
|
+
const decryptdata = mediaKeySessionContext.decryptdata;
|
|
348
|
+
if (decryptdata.pssh) {
|
|
349
|
+
const keySessionContext = this.createMediaKeySessionContext(
|
|
350
|
+
mediaKeySessionContext,
|
|
351
|
+
);
|
|
352
|
+
const keyId = getKeyIdString(decryptdata);
|
|
353
|
+
const scheme = 'cenc';
|
|
354
|
+
this.keyIdToKeySessionPromise[keyId] =
|
|
355
|
+
this.generateRequestWithPreferredKeySession(
|
|
356
|
+
keySessionContext,
|
|
357
|
+
scheme,
|
|
358
|
+
decryptdata.pssh.buffer,
|
|
359
|
+
'expired',
|
|
360
|
+
);
|
|
361
|
+
} else {
|
|
362
|
+
this.warn(`Could not renew expired session. Missing pssh initData.`);
|
|
363
|
+
}
|
|
364
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
365
|
+
this.removeSession(mediaKeySessionContext);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private updateKeySession(
|
|
369
|
+
mediaKeySessionContext: MediaKeySessionContext,
|
|
370
|
+
data: Uint8Array<ArrayBuffer>,
|
|
371
|
+
): Promise<void> {
|
|
372
|
+
const keySession = mediaKeySessionContext.mediaKeysSession;
|
|
373
|
+
this.log(
|
|
374
|
+
`Updating key-session "${keySession.sessionId}" for keyId ${arrayToHex(
|
|
375
|
+
mediaKeySessionContext.decryptdata.keyId || [],
|
|
376
|
+
)}
|
|
377
|
+
} (data length: ${data.byteLength})`,
|
|
378
|
+
);
|
|
379
|
+
return keySession.update(data);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
public getSelectedKeySystemFormats(): KeySystemFormats[] {
|
|
383
|
+
return (Object.keys(this.keySystemAccessPromises) as KeySystems[])
|
|
384
|
+
.map((keySystem) => ({
|
|
385
|
+
keySystem,
|
|
386
|
+
hasMediaKeys: this.keySystemAccessPromises[keySystem]!.hasMediaKeys,
|
|
387
|
+
}))
|
|
388
|
+
.filter(({ hasMediaKeys }) => !!hasMediaKeys)
|
|
389
|
+
.map(({ keySystem }) => keySystemDomainToKeySystemFormat(keySystem))
|
|
390
|
+
.filter((keySystem): keySystem is KeySystemFormats => !!keySystem);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
public getKeySystemAccess(keySystemsToAttempt: KeySystems[]): Promise<void> {
|
|
394
|
+
return this.getKeySystemSelectionPromise(keySystemsToAttempt).then(
|
|
395
|
+
({ keySystem, mediaKeys }) => {
|
|
396
|
+
return this.attemptSetMediaKeys(keySystem, mediaKeys);
|
|
397
|
+
},
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
public selectKeySystem(
|
|
402
|
+
keySystemsToAttempt: KeySystems[],
|
|
403
|
+
): Promise<KeySystemFormats> {
|
|
404
|
+
return new Promise((resolve, reject) => {
|
|
405
|
+
this.getKeySystemSelectionPromise(keySystemsToAttempt)
|
|
406
|
+
.then(({ keySystem }) => {
|
|
407
|
+
const keySystemFormat = keySystemDomainToKeySystemFormat(keySystem);
|
|
408
|
+
if (keySystemFormat) {
|
|
409
|
+
resolve(keySystemFormat);
|
|
410
|
+
} else {
|
|
411
|
+
reject(
|
|
412
|
+
new Error(`Unable to find format for key-system "${keySystem}"`),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
.catch(reject);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
public selectKeySystemFormat(frag: Fragment): Promise<KeySystemFormats> {
|
|
421
|
+
const keyFormats = Object.keys(frag.levelkeys || {}) as KeySystemFormats[];
|
|
422
|
+
if (!this.keyFormatPromise) {
|
|
423
|
+
this.log(
|
|
424
|
+
`Selecting key-system from fragment (sn: ${frag.sn} ${frag.type}: ${
|
|
425
|
+
frag.level
|
|
426
|
+
}) key formats ${keyFormats.join(', ')}`,
|
|
427
|
+
);
|
|
428
|
+
this.keyFormatPromise = this.getKeyFormatPromise(keyFormats);
|
|
429
|
+
}
|
|
430
|
+
return this.keyFormatPromise;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private getKeyFormatPromise(
|
|
434
|
+
keyFormats: KeySystemFormats[],
|
|
435
|
+
): Promise<KeySystemFormats> {
|
|
436
|
+
const keySystemsInConfig = getKeySystemsForConfig(this.config);
|
|
437
|
+
const keySystemsToAttempt = keyFormats
|
|
438
|
+
.map(keySystemFormatToKeySystemDomain)
|
|
439
|
+
.filter(
|
|
440
|
+
(value) => !!value && keySystemsInConfig.indexOf(value) !== -1,
|
|
441
|
+
) as any as KeySystems[];
|
|
442
|
+
|
|
443
|
+
return this.selectKeySystem(keySystemsToAttempt);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
public getKeyStatus(decryptdata: LevelKey): MediaKeyStatus | undefined {
|
|
447
|
+
const { mediaKeySessions } = this;
|
|
448
|
+
for (let i = 0; i < mediaKeySessions.length; i++) {
|
|
449
|
+
const status = getKeyStatus(decryptdata, mediaKeySessions[i]);
|
|
450
|
+
if (status) {
|
|
451
|
+
return status;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
public loadKey(data: KeyLoadedData): Promise<MediaKeySessionContext> {
|
|
458
|
+
const decryptdata = data.keyInfo.decryptdata;
|
|
459
|
+
|
|
460
|
+
const keyId = getKeyIdString(decryptdata);
|
|
461
|
+
const badStatus = this.bannedKeyIds[keyId];
|
|
462
|
+
if (badStatus || this.getKeyStatus(decryptdata) === 'internal-error') {
|
|
463
|
+
const error = getKeyStatusError(
|
|
464
|
+
badStatus || 'internal-error',
|
|
465
|
+
decryptdata,
|
|
466
|
+
);
|
|
467
|
+
this.handleError(error, data.frag);
|
|
468
|
+
return Promise.reject(error);
|
|
469
|
+
}
|
|
470
|
+
const keyDetails = `(keyId: ${keyId} format: "${decryptdata.keyFormat}" method: ${decryptdata.method} uri: ${decryptdata.uri})`;
|
|
471
|
+
|
|
472
|
+
this.log(`Starting session for key ${keyDetails}`);
|
|
473
|
+
|
|
474
|
+
const keyContextPromise = this.keyIdToKeySessionPromise[keyId];
|
|
475
|
+
if (!keyContextPromise) {
|
|
476
|
+
const keySessionContextPromise = this.getKeySystemForKeyPromise(
|
|
477
|
+
decryptdata,
|
|
478
|
+
)
|
|
479
|
+
.then(({ keySystem, mediaKeys }) => {
|
|
480
|
+
this.throwIfDestroyed();
|
|
481
|
+
this.log(
|
|
482
|
+
`Handle encrypted media sn: ${data.frag.sn} ${data.frag.type}: ${data.frag.level} using key ${keyDetails}`,
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => {
|
|
486
|
+
this.throwIfDestroyed();
|
|
487
|
+
return this.createMediaKeySessionContext({
|
|
488
|
+
keySystem,
|
|
489
|
+
mediaKeys,
|
|
490
|
+
decryptdata,
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
})
|
|
494
|
+
.then((keySessionContext) => {
|
|
495
|
+
const scheme = 'cenc';
|
|
496
|
+
const initData = decryptdata.pssh ? decryptdata.pssh.buffer : null;
|
|
497
|
+
return this.generateRequestWithPreferredKeySession(
|
|
498
|
+
keySessionContext,
|
|
499
|
+
scheme,
|
|
500
|
+
initData,
|
|
501
|
+
'playlist-key',
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
keySessionContextPromise.catch((error) =>
|
|
506
|
+
this.handleError(error, data.frag),
|
|
507
|
+
);
|
|
508
|
+
this.keyIdToKeySessionPromise[keyId] = keySessionContextPromise;
|
|
509
|
+
|
|
510
|
+
return keySessionContextPromise;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Re-emit error for playlist key loading
|
|
514
|
+
keyContextPromise.catch((error) => {
|
|
515
|
+
if (error instanceof EMEKeyError) {
|
|
516
|
+
const errorData = { ...error.data };
|
|
517
|
+
if (this.getKeyStatus(decryptdata) === 'internal-error') {
|
|
518
|
+
errorData.decryptdata = decryptdata;
|
|
519
|
+
}
|
|
520
|
+
const clonedError = new EMEKeyError(errorData, error.message);
|
|
521
|
+
this.handleError(clonedError, data.frag);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
return keyContextPromise;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private throwIfDestroyed(message = 'Invalid state'): void | never {
|
|
529
|
+
if (!this.hls as any) {
|
|
530
|
+
throw new Error('invalid state');
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private handleError(error: EMEKeyError | Error, frag?: Fragment) {
|
|
535
|
+
if (!this.hls as any) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (error instanceof EMEKeyError) {
|
|
540
|
+
if (frag) {
|
|
541
|
+
error.data.frag = frag;
|
|
542
|
+
}
|
|
543
|
+
const levelKey = error.data.decryptdata;
|
|
544
|
+
this.error(
|
|
545
|
+
`${error.message}${
|
|
546
|
+
levelKey ? ` (${arrayToHex(levelKey.keyId || [])})` : ''
|
|
547
|
+
}`,
|
|
548
|
+
);
|
|
549
|
+
this.hls.trigger(Events.ERROR, error.data);
|
|
550
|
+
} else {
|
|
551
|
+
this.error(error.message);
|
|
552
|
+
this.hls.trigger(Events.ERROR, {
|
|
553
|
+
type: ErrorTypes.KEY_SYSTEM_ERROR,
|
|
554
|
+
details: ErrorDetails.KEY_SYSTEM_NO_KEYS,
|
|
555
|
+
error,
|
|
556
|
+
fatal: true,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private getKeySystemForKeyPromise(
|
|
562
|
+
decryptdata: LevelKey,
|
|
563
|
+
): Promise<{ keySystem: KeySystems; mediaKeys: MediaKeys }> {
|
|
564
|
+
const keyId = getKeyIdString(decryptdata);
|
|
565
|
+
const mediaKeySessionContext = this.keyIdToKeySessionPromise[keyId];
|
|
566
|
+
if (!mediaKeySessionContext) {
|
|
567
|
+
const keySystem = keySystemFormatToKeySystemDomain(
|
|
568
|
+
decryptdata.keyFormat as KeySystemFormats,
|
|
569
|
+
);
|
|
570
|
+
const keySystemsToAttempt = keySystem
|
|
571
|
+
? [keySystem]
|
|
572
|
+
: getKeySystemsForConfig(this.config);
|
|
573
|
+
return this.attemptKeySystemAccess(keySystemsToAttempt);
|
|
574
|
+
}
|
|
575
|
+
return mediaKeySessionContext;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private getKeySystemSelectionPromise(
|
|
579
|
+
keySystemsToAttempt: KeySystems[],
|
|
580
|
+
): Promise<{ keySystem: KeySystems; mediaKeys: MediaKeys }> | never {
|
|
581
|
+
if (!keySystemsToAttempt.length) {
|
|
582
|
+
keySystemsToAttempt = getKeySystemsForConfig(this.config);
|
|
583
|
+
}
|
|
584
|
+
if (keySystemsToAttempt.length === 0) {
|
|
585
|
+
throw new EMEKeyError(
|
|
586
|
+
{
|
|
587
|
+
type: ErrorTypes.KEY_SYSTEM_ERROR,
|
|
588
|
+
details: ErrorDetails.KEY_SYSTEM_NO_CONFIGURED_LICENSE,
|
|
589
|
+
fatal: true,
|
|
590
|
+
},
|
|
591
|
+
`Missing key-system license configuration options ${stringify({
|
|
592
|
+
drmSystems: this.config.drmSystems,
|
|
593
|
+
})}`,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
return this.attemptKeySystemAccess(keySystemsToAttempt);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private onMediaEncrypted = (event: MediaEncryptedEvent) => {
|
|
600
|
+
const { initDataType, initData } = event;
|
|
601
|
+
const logMessage = `"${event.type}" event: init data type: "${initDataType}"`;
|
|
602
|
+
this.debug(logMessage);
|
|
603
|
+
|
|
604
|
+
// Ignore event when initData is null
|
|
605
|
+
if (initData === null) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (!this.keyFormatPromise) {
|
|
610
|
+
let keySystems = Object.keys(
|
|
611
|
+
this.keySystemAccessPromises,
|
|
612
|
+
) as KeySystems[];
|
|
613
|
+
if (!keySystems.length) {
|
|
614
|
+
keySystems = getKeySystemsForConfig(this.config);
|
|
615
|
+
}
|
|
616
|
+
const keyFormats = keySystems
|
|
617
|
+
.map(keySystemDomainToKeySystemFormat)
|
|
618
|
+
.filter((k) => !!k) as KeySystemFormats[];
|
|
619
|
+
this.keyFormatPromise = this.getKeyFormatPromise(keyFormats);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
this.keyFormatPromise
|
|
623
|
+
.then((keySystemFormat) => {
|
|
624
|
+
const keySystem = keySystemFormatToKeySystemDomain(keySystemFormat);
|
|
625
|
+
if (initDataType !== 'sinf' || keySystem !== KeySystems.FAIRPLAY) {
|
|
626
|
+
this.log(
|
|
627
|
+
`Ignoring "${event.type}" event with init data type: "${initDataType}" for selected key-system ${keySystem}`,
|
|
628
|
+
);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Match sinf keyId to playlist skd://keyId=
|
|
633
|
+
let keyId: Uint8Array<ArrayBuffer> | undefined;
|
|
634
|
+
try {
|
|
635
|
+
const json = bin2str(new Uint8Array(initData));
|
|
636
|
+
const sinf = base64Decode(JSON.parse(json).sinf);
|
|
637
|
+
const tenc = parseSinf(sinf);
|
|
638
|
+
if (!tenc) {
|
|
639
|
+
throw new Error(
|
|
640
|
+
`'schm' box missing or not cbcs/cenc with schi > tenc`,
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
keyId = new Uint8Array(tenc.subarray(8, 24));
|
|
644
|
+
} catch (error) {
|
|
645
|
+
this.warn(`${logMessage} Failed to parse sinf: ${error}`);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const keyIdHex = arrayToHex(keyId);
|
|
650
|
+
const { keyIdToKeySessionPromise, mediaKeySessions } = this;
|
|
651
|
+
let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex];
|
|
652
|
+
|
|
653
|
+
for (let i = 0; i < mediaKeySessions.length; i++) {
|
|
654
|
+
// Match playlist key
|
|
655
|
+
const keyContext = mediaKeySessions[i];
|
|
656
|
+
const decryptdata = keyContext.decryptdata;
|
|
657
|
+
if (!decryptdata.keyId) {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const oldKeyIdHex = arrayToHex(decryptdata.keyId);
|
|
661
|
+
if (
|
|
662
|
+
arrayValuesMatch(keyId, decryptdata.keyId) ||
|
|
663
|
+
decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1
|
|
664
|
+
) {
|
|
665
|
+
keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex];
|
|
666
|
+
if (!keySessionContextPromise) {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (decryptdata.pssh) {
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
delete keyIdToKeySessionPromise[oldKeyIdHex];
|
|
673
|
+
decryptdata.pssh = new Uint8Array(initData);
|
|
674
|
+
decryptdata.keyId = keyId;
|
|
675
|
+
keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] =
|
|
676
|
+
keySessionContextPromise.then(() => {
|
|
677
|
+
return this.generateRequestWithPreferredKeySession(
|
|
678
|
+
keyContext,
|
|
679
|
+
initDataType,
|
|
680
|
+
initData,
|
|
681
|
+
'encrypted-event-key-match',
|
|
682
|
+
);
|
|
683
|
+
});
|
|
684
|
+
keySessionContextPromise.catch((error) => this.handleError(error));
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (!keySessionContextPromise) {
|
|
690
|
+
this.handleError(
|
|
691
|
+
new Error(
|
|
692
|
+
`Key ID ${keyIdHex} not encountered in playlist. Key-system sessions ${mediaKeySessions.length}.`,
|
|
693
|
+
),
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
})
|
|
697
|
+
.catch((error) => this.handleError(error));
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
private onWaitingForKey = (event: Event) => {
|
|
701
|
+
this.log(`"${event.type}" event`);
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
private attemptSetMediaKeys(
|
|
705
|
+
keySystem: KeySystems,
|
|
706
|
+
mediaKeys: MediaKeys,
|
|
707
|
+
): Promise<void> {
|
|
708
|
+
this.mediaResolved = undefined;
|
|
709
|
+
if (this.mediaKeys === mediaKeys) {
|
|
710
|
+
return Promise.resolve();
|
|
711
|
+
}
|
|
712
|
+
const queue = this.setMediaKeysQueue.slice();
|
|
713
|
+
|
|
714
|
+
this.log(`Setting media-keys for "${keySystem}"`);
|
|
715
|
+
// Only one setMediaKeys() can run at one time, and multiple setMediaKeys() operations
|
|
716
|
+
// can be queued for execution for multiple key sessions.
|
|
717
|
+
const setMediaKeysPromise = Promise.all(queue).then(() => {
|
|
718
|
+
if (!this.media) {
|
|
719
|
+
return new Promise((resolve: (value?: void) => void, reject) => {
|
|
720
|
+
this.mediaResolved = () => {
|
|
721
|
+
this.mediaResolved = undefined;
|
|
722
|
+
if (!this.media) {
|
|
723
|
+
return reject(
|
|
724
|
+
new Error(
|
|
725
|
+
'Attempted to set mediaKeys without media element attached',
|
|
726
|
+
),
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
this.mediaKeys = mediaKeys;
|
|
730
|
+
this.media.setMediaKeys(mediaKeys).then(resolve).catch(reject);
|
|
731
|
+
};
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
return this.media.setMediaKeys(mediaKeys);
|
|
735
|
+
});
|
|
736
|
+
this.mediaKeys = mediaKeys;
|
|
737
|
+
this.setMediaKeysQueue.push(setMediaKeysPromise);
|
|
738
|
+
return setMediaKeysPromise.then(() => {
|
|
739
|
+
this.log(`Media-keys set for "${keySystem}"`);
|
|
740
|
+
queue.push(setMediaKeysPromise!);
|
|
741
|
+
this.setMediaKeysQueue = this.setMediaKeysQueue.filter(
|
|
742
|
+
(p) => queue.indexOf(p) === -1,
|
|
743
|
+
);
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private generateRequestWithPreferredKeySession(
|
|
748
|
+
context: MediaKeySessionContext,
|
|
749
|
+
initDataType: string,
|
|
750
|
+
initData: ArrayBuffer | null,
|
|
751
|
+
reason:
|
|
752
|
+
| 'playlist-key'
|
|
753
|
+
| 'encrypted-event-key-match'
|
|
754
|
+
| 'encrypted-event-no-match'
|
|
755
|
+
| 'expired',
|
|
756
|
+
): Promise<MediaKeySessionContext> | never {
|
|
757
|
+
const generateRequestFilter =
|
|
758
|
+
this.config.drmSystems?.[context.keySystem]?.generateRequest;
|
|
759
|
+
if (generateRequestFilter) {
|
|
760
|
+
try {
|
|
761
|
+
const mappedInitData: ReturnType<typeof generateRequestFilter> =
|
|
762
|
+
generateRequestFilter.call(this.hls, initDataType, initData, context);
|
|
763
|
+
if (!mappedInitData) {
|
|
764
|
+
throw new Error(
|
|
765
|
+
'Invalid response from configured generateRequest filter',
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
initDataType = mappedInitData.initDataType;
|
|
769
|
+
initData = mappedInitData.initData ? mappedInitData.initData : null;
|
|
770
|
+
context.decryptdata.pssh = initData ? new Uint8Array(initData) : null;
|
|
771
|
+
} catch (error) {
|
|
772
|
+
this.warn(error.message);
|
|
773
|
+
if ((this.hls as any) && this.hls.config.debug) {
|
|
774
|
+
throw error;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (initData === null) {
|
|
780
|
+
this.log(`Skipping key-session request for "${reason}" (no initData)`);
|
|
781
|
+
return Promise.resolve(context);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const keyId = getKeyIdString(context.decryptdata);
|
|
785
|
+
const keyUri = context.decryptdata.uri;
|
|
786
|
+
this.log(
|
|
787
|
+
`Generating key-session request for "${reason}" keyId: ${keyId} URI: ${keyUri} (init data type: ${initDataType} length: ${
|
|
788
|
+
initData.byteLength
|
|
789
|
+
})`,
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
const licenseStatus = new EventEmitter();
|
|
793
|
+
|
|
794
|
+
const onmessage = (context._onmessage = (event: MediaKeyMessageEvent) => {
|
|
795
|
+
const keySession = context.mediaKeysSession;
|
|
796
|
+
if (!keySession as any) {
|
|
797
|
+
licenseStatus.emit('error', new Error('invalid state'));
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const { messageType, message } = event;
|
|
801
|
+
this.log(
|
|
802
|
+
`"${messageType}" message event for session "${keySession.sessionId}" message size: ${message.byteLength}`,
|
|
803
|
+
);
|
|
804
|
+
if (
|
|
805
|
+
messageType === 'license-request' ||
|
|
806
|
+
messageType === 'license-renewal'
|
|
807
|
+
) {
|
|
808
|
+
this.renewLicense(context, message).catch((error) => {
|
|
809
|
+
if (licenseStatus.eventNames().length) {
|
|
810
|
+
licenseStatus.emit('error', error);
|
|
811
|
+
} else {
|
|
812
|
+
this.handleError(error);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
} else if (messageType === 'license-release') {
|
|
816
|
+
if (context.keySystem === KeySystems.FAIRPLAY) {
|
|
817
|
+
this.updateKeySession(context, strToUtf8array('acknowledged'))
|
|
818
|
+
.then(() => this.removeSession(context))
|
|
819
|
+
.catch((error) => this.handleError(error));
|
|
820
|
+
}
|
|
821
|
+
} else {
|
|
822
|
+
this.warn(`unhandled media key message type "${messageType}"`);
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
const handleKeyStatus = (
|
|
827
|
+
keyStatus: MediaKeyStatus,
|
|
828
|
+
context: MediaKeySessionContext,
|
|
829
|
+
) => {
|
|
830
|
+
context.keyStatus = keyStatus;
|
|
831
|
+
let keyError: EMEKeyError | Error | undefined;
|
|
832
|
+
if (keyStatus.startsWith('usable')) {
|
|
833
|
+
licenseStatus.emit('resolved');
|
|
834
|
+
} else if (
|
|
835
|
+
keyStatus === 'internal-error' ||
|
|
836
|
+
keyStatus === 'output-restricted' ||
|
|
837
|
+
keyStatus === 'output-downscaled'
|
|
838
|
+
) {
|
|
839
|
+
keyError = getKeyStatusError(keyStatus, context.decryptdata);
|
|
840
|
+
} else if (keyStatus === 'expired') {
|
|
841
|
+
keyError = new Error(`key expired (keyId: ${keyId})`);
|
|
842
|
+
} else if (keyStatus === 'released') {
|
|
843
|
+
keyError = new Error(`key released`);
|
|
844
|
+
} else if (keyStatus === 'status-pending') {
|
|
845
|
+
/* no-op */
|
|
846
|
+
} else {
|
|
847
|
+
this.warn(
|
|
848
|
+
`unhandled key status change "${keyStatus}" (keyId: ${keyId})`,
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
if (keyError) {
|
|
852
|
+
if (licenseStatus.eventNames().length) {
|
|
853
|
+
licenseStatus.emit('error', keyError);
|
|
854
|
+
} else {
|
|
855
|
+
this.handleError(keyError);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
const onkeystatuseschange = (context._onkeystatuseschange = (
|
|
860
|
+
event: Event,
|
|
861
|
+
) => {
|
|
862
|
+
const keySession = context.mediaKeysSession;
|
|
863
|
+
if (!keySession as any) {
|
|
864
|
+
licenseStatus.emit('error', new Error('invalid state'));
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const keyStatuses = this.getKeyStatuses(context);
|
|
869
|
+
const keyIds = Object.keys(keyStatuses);
|
|
870
|
+
|
|
871
|
+
// exit if all keys are status-pending
|
|
872
|
+
if (!keyIds.some((id) => keyStatuses[id] !== 'status-pending')) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// renew when a key status for a levelKey comes back expired
|
|
877
|
+
if (keyStatuses[keyId] === 'expired') {
|
|
878
|
+
// renew when a key status comes back expired
|
|
879
|
+
this.log(
|
|
880
|
+
`Expired key ${stringify(keyStatuses)} in key-session "${context.mediaKeysSession.sessionId}"`,
|
|
881
|
+
);
|
|
882
|
+
this.renewKeySession(context);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
let keyStatus = keyStatuses[keyId] as MediaKeyStatus | undefined;
|
|
887
|
+
if (keyStatus) {
|
|
888
|
+
// handle status of current key
|
|
889
|
+
handleKeyStatus(keyStatus, context);
|
|
890
|
+
} else {
|
|
891
|
+
// Timeout key-status
|
|
892
|
+
const timeout = 1000;
|
|
893
|
+
context.keyStatusTimeouts ||= {};
|
|
894
|
+
context.keyStatusTimeouts[keyId] ||= self.setTimeout(() => {
|
|
895
|
+
if ((!context.mediaKeysSession as any) || !this.mediaKeys) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Find key status in another session if missing (PlayReady #7519 no key-status "single-key" setup with shared key)
|
|
900
|
+
const sessionKeyStatus = this.getKeyStatus(context.decryptdata);
|
|
901
|
+
if (sessionKeyStatus && sessionKeyStatus !== 'status-pending') {
|
|
902
|
+
this.log(
|
|
903
|
+
`No status for keyId ${keyId} in key-session "${context.mediaKeysSession.sessionId}". Using session key-status ${sessionKeyStatus} from other session.`,
|
|
904
|
+
);
|
|
905
|
+
return handleKeyStatus(sessionKeyStatus, context);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Timeout key with internal-error
|
|
909
|
+
this.log(
|
|
910
|
+
`key status for ${keyId} in key-session "${context.mediaKeysSession.sessionId}" timed out after ${timeout}ms`,
|
|
911
|
+
);
|
|
912
|
+
keyStatus = 'internal-error';
|
|
913
|
+
handleKeyStatus(keyStatus, context);
|
|
914
|
+
}, timeout);
|
|
915
|
+
|
|
916
|
+
this.log(`No status for keyId ${keyId} (${stringify(keyStatuses)}).`);
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
addEventListener(context.mediaKeysSession, 'message', onmessage);
|
|
921
|
+
addEventListener(
|
|
922
|
+
context.mediaKeysSession,
|
|
923
|
+
'keystatuseschange',
|
|
924
|
+
onkeystatuseschange,
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
const keyUsablePromise = new Promise(
|
|
928
|
+
(resolve: (value?: void) => void, reject) => {
|
|
929
|
+
licenseStatus.on('error', reject);
|
|
930
|
+
licenseStatus.on('resolved', resolve);
|
|
931
|
+
},
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
return context.mediaKeysSession
|
|
935
|
+
.generateRequest(initDataType, initData)
|
|
936
|
+
.then(() => {
|
|
937
|
+
this.log(
|
|
938
|
+
`Request generated for key-session "${context.mediaKeysSession.sessionId}" keyId: ${keyId} URI: ${keyUri}`,
|
|
939
|
+
);
|
|
940
|
+
})
|
|
941
|
+
.catch((error) => {
|
|
942
|
+
throw new EMEKeyError(
|
|
943
|
+
{
|
|
944
|
+
type: ErrorTypes.KEY_SYSTEM_ERROR,
|
|
945
|
+
details: ErrorDetails.KEY_SYSTEM_NO_SESSION,
|
|
946
|
+
error,
|
|
947
|
+
decryptdata: context.decryptdata,
|
|
948
|
+
fatal: false,
|
|
949
|
+
},
|
|
950
|
+
`Error generating key-session request: ${error}`,
|
|
951
|
+
);
|
|
952
|
+
})
|
|
953
|
+
.then(() => keyUsablePromise)
|
|
954
|
+
.catch((error) => {
|
|
955
|
+
licenseStatus.removeAllListeners();
|
|
956
|
+
return this.removeSession(context).then(() => {
|
|
957
|
+
throw error;
|
|
958
|
+
});
|
|
959
|
+
})
|
|
960
|
+
.then(() => {
|
|
961
|
+
licenseStatus.removeAllListeners();
|
|
962
|
+
return context;
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
private getKeyStatuses(mediaKeySessionContext: MediaKeySessionContext): {
|
|
967
|
+
[keyId: string]: MediaKeyStatus;
|
|
968
|
+
} {
|
|
969
|
+
const keyStatuses: { [keyId: string]: MediaKeyStatus } = {};
|
|
970
|
+
mediaKeySessionContext.mediaKeysSession.keyStatuses.forEach(
|
|
971
|
+
(status: MediaKeyStatus, keyId: BufferSource) => {
|
|
972
|
+
// keyStatuses.forEach is not standard API so the callback value looks weird on xboxone
|
|
973
|
+
// xboxone callback(keyId, status) so we need to exchange them
|
|
974
|
+
if (typeof keyId === 'string' && typeof status === 'object') {
|
|
975
|
+
const temp = keyId;
|
|
976
|
+
keyId = status;
|
|
977
|
+
status = temp;
|
|
978
|
+
}
|
|
979
|
+
const keyIdArray =
|
|
980
|
+
'buffer' in keyId
|
|
981
|
+
? new Uint8Array(keyId.buffer, keyId.byteOffset, keyId.byteLength)
|
|
982
|
+
: new Uint8Array(keyId);
|
|
983
|
+
|
|
984
|
+
if (
|
|
985
|
+
mediaKeySessionContext.keySystem === KeySystems.PLAYREADY &&
|
|
986
|
+
keyIdArray.length === 16
|
|
987
|
+
) {
|
|
988
|
+
// On some devices, the key ID has already been converted for endianness.
|
|
989
|
+
// In such cases, this key ID is the one we need to cache.
|
|
990
|
+
const originKeyIdWithStatusChange = arrayToHex(keyIdArray);
|
|
991
|
+
// Cache the original key IDs to ensure compatibility across all cases.
|
|
992
|
+
keyStatuses[originKeyIdWithStatusChange] = status;
|
|
993
|
+
|
|
994
|
+
changeEndianness(keyIdArray);
|
|
995
|
+
}
|
|
996
|
+
const keyIdWithStatusChange = arrayToHex(keyIdArray);
|
|
997
|
+
// Add to banned keys to prevent playlist usage and license requests
|
|
998
|
+
if (status === 'internal-error') {
|
|
999
|
+
this.bannedKeyIds[keyIdWithStatusChange] = status;
|
|
1000
|
+
}
|
|
1001
|
+
this.log(
|
|
1002
|
+
`key status change "${status}" for keyStatuses keyId: ${keyIdWithStatusChange} key-session "${mediaKeySessionContext.mediaKeysSession.sessionId}"`,
|
|
1003
|
+
);
|
|
1004
|
+
|
|
1005
|
+
keyStatuses[keyIdWithStatusChange] = status;
|
|
1006
|
+
},
|
|
1007
|
+
);
|
|
1008
|
+
return keyStatuses;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
private fetchServerCertificate(
|
|
1012
|
+
keySystem: KeySystems,
|
|
1013
|
+
): Promise<BufferSource | void> {
|
|
1014
|
+
const config = this.config;
|
|
1015
|
+
const Loader = config.loader;
|
|
1016
|
+
const certLoader = new Loader(config as HlsConfig) as Loader<LoaderContext>;
|
|
1017
|
+
const url = this.getServerCertificateUrl(keySystem);
|
|
1018
|
+
if (!url) {
|
|
1019
|
+
return Promise.resolve();
|
|
1020
|
+
}
|
|
1021
|
+
this.log(`Fetching server certificate for "${keySystem}"`);
|
|
1022
|
+
return new Promise((resolve, reject) => {
|
|
1023
|
+
const loaderContext: LoaderContext = {
|
|
1024
|
+
responseType: 'arraybuffer',
|
|
1025
|
+
url,
|
|
1026
|
+
};
|
|
1027
|
+
const loadPolicy = config.certLoadPolicy.default;
|
|
1028
|
+
const loaderConfig: LoaderConfiguration = {
|
|
1029
|
+
loadPolicy,
|
|
1030
|
+
timeout: loadPolicy.maxLoadTimeMs,
|
|
1031
|
+
maxRetry: 0,
|
|
1032
|
+
retryDelay: 0,
|
|
1033
|
+
maxRetryDelay: 0,
|
|
1034
|
+
};
|
|
1035
|
+
const loaderCallbacks: LoaderCallbacks<LoaderContext> = {
|
|
1036
|
+
onSuccess: (response, stats, context, networkDetails) => {
|
|
1037
|
+
resolve(response.data as ArrayBuffer);
|
|
1038
|
+
},
|
|
1039
|
+
onError: (response, contex, networkDetails, stats) => {
|
|
1040
|
+
reject(
|
|
1041
|
+
new EMEKeyError(
|
|
1042
|
+
{
|
|
1043
|
+
type: ErrorTypes.KEY_SYSTEM_ERROR,
|
|
1044
|
+
details:
|
|
1045
|
+
ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED,
|
|
1046
|
+
fatal: true,
|
|
1047
|
+
networkDetails,
|
|
1048
|
+
response: {
|
|
1049
|
+
url: loaderContext.url,
|
|
1050
|
+
data: undefined,
|
|
1051
|
+
...response,
|
|
1052
|
+
},
|
|
1053
|
+
},
|
|
1054
|
+
`"${keySystem}" certificate request failed (${url}). Status: ${response.code} (${response.text})`,
|
|
1055
|
+
),
|
|
1056
|
+
);
|
|
1057
|
+
},
|
|
1058
|
+
onTimeout: (stats, context, networkDetails) => {
|
|
1059
|
+
reject(
|
|
1060
|
+
new EMEKeyError(
|
|
1061
|
+
{
|
|
1062
|
+
type: ErrorTypes.KEY_SYSTEM_ERROR,
|
|
1063
|
+
details:
|
|
1064
|
+
ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED,
|
|
1065
|
+
fatal: true,
|
|
1066
|
+
networkDetails,
|
|
1067
|
+
response: {
|
|
1068
|
+
url: loaderContext.url,
|
|
1069
|
+
data: undefined,
|
|
1070
|
+
},
|
|
1071
|
+
},
|
|
1072
|
+
`"${keySystem}" certificate request timed out (${url})`,
|
|
1073
|
+
),
|
|
1074
|
+
);
|
|
1075
|
+
},
|
|
1076
|
+
onAbort: (stats, context, networkDetails) => {
|
|
1077
|
+
reject(new Error('aborted'));
|
|
1078
|
+
},
|
|
1079
|
+
};
|
|
1080
|
+
certLoader.load(loaderContext, loaderConfig, loaderCallbacks);
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
private setMediaKeysServerCertificate(
|
|
1085
|
+
mediaKeys: MediaKeys,
|
|
1086
|
+
keySystem: KeySystems,
|
|
1087
|
+
cert: BufferSource,
|
|
1088
|
+
): Promise<MediaKeys> {
|
|
1089
|
+
return new Promise((resolve, reject) => {
|
|
1090
|
+
mediaKeys
|
|
1091
|
+
.setServerCertificate(cert)
|
|
1092
|
+
.then((success) => {
|
|
1093
|
+
this.log(
|
|
1094
|
+
`setServerCertificate ${
|
|
1095
|
+
success ? 'success' : 'not supported by CDM'
|
|
1096
|
+
} (${cert.byteLength}) on "${keySystem}"`,
|
|
1097
|
+
);
|
|
1098
|
+
resolve(mediaKeys);
|
|
1099
|
+
})
|
|
1100
|
+
.catch((error) => {
|
|
1101
|
+
reject(
|
|
1102
|
+
new EMEKeyError(
|
|
1103
|
+
{
|
|
1104
|
+
type: ErrorTypes.KEY_SYSTEM_ERROR,
|
|
1105
|
+
details:
|
|
1106
|
+
ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED,
|
|
1107
|
+
error,
|
|
1108
|
+
fatal: true,
|
|
1109
|
+
},
|
|
1110
|
+
error.message,
|
|
1111
|
+
),
|
|
1112
|
+
);
|
|
1113
|
+
});
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
private renewLicense(
|
|
1118
|
+
context: MediaKeySessionContext,
|
|
1119
|
+
keyMessage: ArrayBuffer,
|
|
1120
|
+
): Promise<void> {
|
|
1121
|
+
return this.requestLicense(context, new Uint8Array(keyMessage)).then(
|
|
1122
|
+
(data: ArrayBuffer) => {
|
|
1123
|
+
return this.updateKeySession(context, new Uint8Array(data)).catch(
|
|
1124
|
+
(error) => {
|
|
1125
|
+
throw new EMEKeyError(
|
|
1126
|
+
{
|
|
1127
|
+
type: ErrorTypes.KEY_SYSTEM_ERROR,
|
|
1128
|
+
details: ErrorDetails.KEY_SYSTEM_SESSION_UPDATE_FAILED,
|
|
1129
|
+
decryptdata: context.decryptdata,
|
|
1130
|
+
error,
|
|
1131
|
+
fatal: false,
|
|
1132
|
+
},
|
|
1133
|
+
error.message,
|
|
1134
|
+
);
|
|
1135
|
+
},
|
|
1136
|
+
);
|
|
1137
|
+
},
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
private unpackPlayReadyKeyMessage(
|
|
1142
|
+
xhr: XMLHttpRequest,
|
|
1143
|
+
licenseChallenge: Uint8Array<ArrayBuffer>,
|
|
1144
|
+
): Uint8Array<ArrayBuffer> {
|
|
1145
|
+
// On Edge, the raw license message is UTF-16-encoded XML. We need
|
|
1146
|
+
// to unpack the Challenge element (base64-encoded string containing the
|
|
1147
|
+
// actual license request) and any HttpHeader elements (sent as request
|
|
1148
|
+
// headers).
|
|
1149
|
+
// For PlayReady CDMs, we need to dig the Challenge out of the XML.
|
|
1150
|
+
const xmlString = String.fromCharCode.apply(
|
|
1151
|
+
null,
|
|
1152
|
+
new Uint16Array(licenseChallenge.buffer),
|
|
1153
|
+
);
|
|
1154
|
+
if (!xmlString.includes('PlayReadyKeyMessage')) {
|
|
1155
|
+
// This does not appear to be a wrapped message as on Edge. Some
|
|
1156
|
+
// clients do not need this unwrapping, so we will assume this is one of
|
|
1157
|
+
// them. Note that "xml" at this point probably looks like random
|
|
1158
|
+
// garbage, since we interpreted UTF-8 as UTF-16.
|
|
1159
|
+
xhr.setRequestHeader('Content-Type', 'text/xml; charset=utf-8');
|
|
1160
|
+
return licenseChallenge;
|
|
1161
|
+
}
|
|
1162
|
+
const keyMessageXml = new DOMParser().parseFromString(
|
|
1163
|
+
xmlString,
|
|
1164
|
+
'application/xml',
|
|
1165
|
+
);
|
|
1166
|
+
// Set request headers.
|
|
1167
|
+
const headers = keyMessageXml.querySelectorAll('HttpHeader');
|
|
1168
|
+
if (headers.length > 0) {
|
|
1169
|
+
let header: Element;
|
|
1170
|
+
for (let i = 0, len = headers.length; i < len; i++) {
|
|
1171
|
+
header = headers[i];
|
|
1172
|
+
const name = header.querySelector('name')?.textContent;
|
|
1173
|
+
const value = header.querySelector('value')?.textContent;
|
|
1174
|
+
if (name && value) {
|
|
1175
|
+
xhr.setRequestHeader(name, value);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
const challengeElement = keyMessageXml.querySelector('Challenge');
|
|
1180
|
+
const challengeText = challengeElement?.textContent;
|
|
1181
|
+
if (!challengeText) {
|
|
1182
|
+
throw new Error(`Cannot find <Challenge> in key message`);
|
|
1183
|
+
}
|
|
1184
|
+
return strToUtf8array(atob(challengeText));
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
private setupLicenseXHR(
|
|
1188
|
+
xhr: XMLHttpRequest,
|
|
1189
|
+
url: string,
|
|
1190
|
+
keysListItem: MediaKeySessionContext,
|
|
1191
|
+
licenseChallenge: Uint8Array<ArrayBuffer>,
|
|
1192
|
+
): Promise<{
|
|
1193
|
+
xhr: XMLHttpRequest;
|
|
1194
|
+
licenseChallenge: Uint8Array<ArrayBuffer>;
|
|
1195
|
+
}> {
|
|
1196
|
+
const licenseXhrSetup = this.config.licenseXhrSetup;
|
|
1197
|
+
|
|
1198
|
+
if (!licenseXhrSetup) {
|
|
1199
|
+
xhr.open('POST', url, true);
|
|
1200
|
+
|
|
1201
|
+
return Promise.resolve({ xhr, licenseChallenge });
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
return Promise.resolve()
|
|
1205
|
+
.then(() => {
|
|
1206
|
+
if (!keysListItem.decryptdata as any) {
|
|
1207
|
+
throw new Error('Key removed');
|
|
1208
|
+
}
|
|
1209
|
+
return licenseXhrSetup.call(
|
|
1210
|
+
this.hls,
|
|
1211
|
+
xhr,
|
|
1212
|
+
url,
|
|
1213
|
+
keysListItem,
|
|
1214
|
+
licenseChallenge,
|
|
1215
|
+
);
|
|
1216
|
+
})
|
|
1217
|
+
.catch((error: Error) => {
|
|
1218
|
+
if (!keysListItem.decryptdata as any) {
|
|
1219
|
+
// Key session removed. Cancel license request.
|
|
1220
|
+
throw error;
|
|
1221
|
+
}
|
|
1222
|
+
// let's try to open before running setup
|
|
1223
|
+
xhr.open('POST', url, true);
|
|
1224
|
+
|
|
1225
|
+
return licenseXhrSetup.call(
|
|
1226
|
+
this.hls,
|
|
1227
|
+
xhr,
|
|
1228
|
+
url,
|
|
1229
|
+
keysListItem,
|
|
1230
|
+
licenseChallenge,
|
|
1231
|
+
);
|
|
1232
|
+
})
|
|
1233
|
+
.then((licenseXhrSetupResult) => {
|
|
1234
|
+
// if licenseXhrSetup did not yet call open, let's do it now
|
|
1235
|
+
if (!xhr.readyState) {
|
|
1236
|
+
xhr.open('POST', url, true);
|
|
1237
|
+
}
|
|
1238
|
+
const finalLicenseChallenge = licenseXhrSetupResult
|
|
1239
|
+
? licenseXhrSetupResult
|
|
1240
|
+
: licenseChallenge;
|
|
1241
|
+
return { xhr, licenseChallenge: finalLicenseChallenge };
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
private requestLicense(
|
|
1246
|
+
keySessionContext: MediaKeySessionContext,
|
|
1247
|
+
licenseChallenge: Uint8Array<ArrayBuffer>,
|
|
1248
|
+
): Promise<ArrayBuffer> {
|
|
1249
|
+
const keyLoadPolicy = this.config.keyLoadPolicy.default;
|
|
1250
|
+
return new Promise((resolve, reject) => {
|
|
1251
|
+
const url = this.getLicenseServerUrlOrThrow(keySessionContext.keySystem);
|
|
1252
|
+
this.log(`Sending license request to URL: ${url}`);
|
|
1253
|
+
const xhr = new XMLHttpRequest();
|
|
1254
|
+
xhr.responseType = 'arraybuffer';
|
|
1255
|
+
xhr.onreadystatechange = () => {
|
|
1256
|
+
if (
|
|
1257
|
+
(!this.hls as any) ||
|
|
1258
|
+
(!keySessionContext.mediaKeysSession as any)
|
|
1259
|
+
) {
|
|
1260
|
+
return reject(new Error('invalid state'));
|
|
1261
|
+
}
|
|
1262
|
+
if (xhr.readyState === 4) {
|
|
1263
|
+
if (xhr.status === 200) {
|
|
1264
|
+
this._requestLicenseFailureCount = 0;
|
|
1265
|
+
let data = xhr.response;
|
|
1266
|
+
this.log(
|
|
1267
|
+
`License received ${
|
|
1268
|
+
data instanceof ArrayBuffer ? data.byteLength : data
|
|
1269
|
+
}`,
|
|
1270
|
+
);
|
|
1271
|
+
const licenseResponseCallback = this.config.licenseResponseCallback;
|
|
1272
|
+
if (licenseResponseCallback) {
|
|
1273
|
+
try {
|
|
1274
|
+
data = licenseResponseCallback.call(
|
|
1275
|
+
this.hls,
|
|
1276
|
+
xhr,
|
|
1277
|
+
url,
|
|
1278
|
+
keySessionContext,
|
|
1279
|
+
);
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
this.error(error);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
resolve(data);
|
|
1285
|
+
} else {
|
|
1286
|
+
const retryConfig = keyLoadPolicy.errorRetry;
|
|
1287
|
+
const maxNumRetry = retryConfig ? retryConfig.maxNumRetry : 0;
|
|
1288
|
+
this._requestLicenseFailureCount++;
|
|
1289
|
+
if (
|
|
1290
|
+
this._requestLicenseFailureCount > maxNumRetry ||
|
|
1291
|
+
(xhr.status >= 400 && xhr.status < 500)
|
|
1292
|
+
) {
|
|
1293
|
+
reject(
|
|
1294
|
+
new EMEKeyError(
|
|
1295
|
+
{
|
|
1296
|
+
type: ErrorTypes.KEY_SYSTEM_ERROR,
|
|
1297
|
+
details: ErrorDetails.KEY_SYSTEM_LICENSE_REQUEST_FAILED,
|
|
1298
|
+
decryptdata: keySessionContext.decryptdata,
|
|
1299
|
+
fatal: true,
|
|
1300
|
+
networkDetails: xhr,
|
|
1301
|
+
response: {
|
|
1302
|
+
url,
|
|
1303
|
+
data: undefined as any,
|
|
1304
|
+
code: xhr.status,
|
|
1305
|
+
text: xhr.statusText,
|
|
1306
|
+
},
|
|
1307
|
+
},
|
|
1308
|
+
`License Request XHR failed (${url}). Status: ${xhr.status} (${xhr.statusText})`,
|
|
1309
|
+
),
|
|
1310
|
+
);
|
|
1311
|
+
} else {
|
|
1312
|
+
const attemptsLeft =
|
|
1313
|
+
maxNumRetry - this._requestLicenseFailureCount + 1;
|
|
1314
|
+
this.warn(
|
|
1315
|
+
`Retrying license request, ${attemptsLeft} attempts left`,
|
|
1316
|
+
);
|
|
1317
|
+
this.requestLicense(keySessionContext, licenseChallenge).then(
|
|
1318
|
+
resolve,
|
|
1319
|
+
reject,
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
if (
|
|
1326
|
+
keySessionContext.licenseXhr &&
|
|
1327
|
+
keySessionContext.licenseXhr.readyState !== XMLHttpRequest.DONE
|
|
1328
|
+
) {
|
|
1329
|
+
keySessionContext.licenseXhr.abort();
|
|
1330
|
+
}
|
|
1331
|
+
keySessionContext.licenseXhr = xhr;
|
|
1332
|
+
|
|
1333
|
+
this.setupLicenseXHR(xhr, url, keySessionContext, licenseChallenge)
|
|
1334
|
+
.then(({ xhr, licenseChallenge }) => {
|
|
1335
|
+
if (keySessionContext.keySystem == KeySystems.PLAYREADY) {
|
|
1336
|
+
licenseChallenge = this.unpackPlayReadyKeyMessage(
|
|
1337
|
+
xhr,
|
|
1338
|
+
licenseChallenge,
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
xhr.send(licenseChallenge);
|
|
1342
|
+
})
|
|
1343
|
+
.catch(reject);
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
private onDestroying() {
|
|
1348
|
+
this.unregisterListeners();
|
|
1349
|
+
this._clear();
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
private onMediaAttached(
|
|
1353
|
+
event: Events.MEDIA_ATTACHED,
|
|
1354
|
+
data: MediaAttachedData,
|
|
1355
|
+
) {
|
|
1356
|
+
if (!this.config.emeEnabled) {
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const media = data.media;
|
|
1361
|
+
|
|
1362
|
+
// keep reference of media
|
|
1363
|
+
this.media = media;
|
|
1364
|
+
|
|
1365
|
+
addEventListener(media, 'encrypted', this.onMediaEncrypted);
|
|
1366
|
+
addEventListener(media, 'waitingforkey', this.onWaitingForKey);
|
|
1367
|
+
|
|
1368
|
+
const mediaResolved = this.mediaResolved;
|
|
1369
|
+
if (mediaResolved) {
|
|
1370
|
+
mediaResolved();
|
|
1371
|
+
} else {
|
|
1372
|
+
this.mediaKeys = media.mediaKeys;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
private onMediaDetached() {
|
|
1377
|
+
const media = this.media;
|
|
1378
|
+
|
|
1379
|
+
if (media) {
|
|
1380
|
+
removeEventListener(media, 'encrypted', this.onMediaEncrypted);
|
|
1381
|
+
removeEventListener(media, 'waitingforkey', this.onWaitingForKey);
|
|
1382
|
+
this.media = null;
|
|
1383
|
+
this.mediaKeys = null;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
private _clear() {
|
|
1388
|
+
this._requestLicenseFailureCount = 0;
|
|
1389
|
+
this.keyIdToKeySessionPromise = {};
|
|
1390
|
+
this.bannedKeyIds = {};
|
|
1391
|
+
const mediaResolved = this.mediaResolved;
|
|
1392
|
+
if (mediaResolved) {
|
|
1393
|
+
mediaResolved();
|
|
1394
|
+
}
|
|
1395
|
+
if (!this.mediaKeys && !this.mediaKeySessions.length) {
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const media = this.media;
|
|
1399
|
+
const mediaKeysList = this.mediaKeySessions.slice();
|
|
1400
|
+
this.mediaKeySessions = [];
|
|
1401
|
+
this.mediaKeys = null;
|
|
1402
|
+
|
|
1403
|
+
LevelKey.clearKeyUriToKeyIdMap();
|
|
1404
|
+
|
|
1405
|
+
// Close all sessions and remove media keys from the video element.
|
|
1406
|
+
const keySessionCount = mediaKeysList.length;
|
|
1407
|
+
EMEController.CDMCleanupPromise = Promise.all(
|
|
1408
|
+
mediaKeysList
|
|
1409
|
+
.map((mediaKeySessionContext) =>
|
|
1410
|
+
this.removeSession(mediaKeySessionContext),
|
|
1411
|
+
)
|
|
1412
|
+
.concat(
|
|
1413
|
+
(media?.setMediaKeys(null) as Promise<void> | null)?.catch(
|
|
1414
|
+
(error) => {
|
|
1415
|
+
this.log(`Could not clear media keys: ${error}`);
|
|
1416
|
+
if (!this.hls as any) return;
|
|
1417
|
+
this.hls.trigger(Events.ERROR, {
|
|
1418
|
+
type: ErrorTypes.OTHER_ERROR,
|
|
1419
|
+
details: ErrorDetails.KEY_SYSTEM_DESTROY_MEDIA_KEYS_ERROR,
|
|
1420
|
+
fatal: false,
|
|
1421
|
+
error: new Error(`Could not clear media keys: ${error}`),
|
|
1422
|
+
});
|
|
1423
|
+
},
|
|
1424
|
+
) || Promise.resolve(),
|
|
1425
|
+
),
|
|
1426
|
+
)
|
|
1427
|
+
.catch((error) => {
|
|
1428
|
+
this.log(`Could not close sessions and clear media keys: ${error}`);
|
|
1429
|
+
if (!this.hls as any) return;
|
|
1430
|
+
this.hls.trigger(Events.ERROR, {
|
|
1431
|
+
type: ErrorTypes.OTHER_ERROR,
|
|
1432
|
+
details: ErrorDetails.KEY_SYSTEM_DESTROY_CLOSE_SESSION_ERROR,
|
|
1433
|
+
fatal: false,
|
|
1434
|
+
error: new Error(
|
|
1435
|
+
`Could not close sessions and clear media keys: ${error}`,
|
|
1436
|
+
),
|
|
1437
|
+
});
|
|
1438
|
+
})
|
|
1439
|
+
|
|
1440
|
+
.then(() => {
|
|
1441
|
+
if (keySessionCount) {
|
|
1442
|
+
this.log('finished closing key sessions and clearing media keys');
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
private onManifestLoading() {
|
|
1448
|
+
this._clear();
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
private onManifestLoaded(
|
|
1452
|
+
event: Events.MANIFEST_LOADED,
|
|
1453
|
+
{ sessionKeys }: ManifestLoadedData,
|
|
1454
|
+
) {
|
|
1455
|
+
if (!sessionKeys || !this.config.emeEnabled) {
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if (!this.keyFormatPromise) {
|
|
1459
|
+
const keyFormats: KeySystemFormats[] = sessionKeys.reduce(
|
|
1460
|
+
(formats: KeySystemFormats[], sessionKey: LevelKey) => {
|
|
1461
|
+
if (
|
|
1462
|
+
formats.indexOf(sessionKey.keyFormat as KeySystemFormats) === -1
|
|
1463
|
+
) {
|
|
1464
|
+
formats.push(sessionKey.keyFormat as KeySystemFormats);
|
|
1465
|
+
}
|
|
1466
|
+
return formats;
|
|
1467
|
+
},
|
|
1468
|
+
[],
|
|
1469
|
+
);
|
|
1470
|
+
this.log(
|
|
1471
|
+
`Selecting key-system from session-keys ${keyFormats.join(', ')}`,
|
|
1472
|
+
);
|
|
1473
|
+
this.keyFormatPromise = this.getKeyFormatPromise(keyFormats);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
private removeSession(
|
|
1478
|
+
mediaKeySessionContext: MediaKeySessionContext,
|
|
1479
|
+
): Promise<void> {
|
|
1480
|
+
const { mediaKeysSession, licenseXhr, decryptdata } =
|
|
1481
|
+
mediaKeySessionContext;
|
|
1482
|
+
if (mediaKeysSession as MediaKeySession | undefined) {
|
|
1483
|
+
this.log(
|
|
1484
|
+
`Remove licenses and keys and close session "${mediaKeysSession.sessionId}" keyId: ${arrayToHex((decryptdata as LevelKey | undefined)?.keyId || [])}`,
|
|
1485
|
+
);
|
|
1486
|
+
if (mediaKeySessionContext._onmessage) {
|
|
1487
|
+
mediaKeysSession.removeEventListener(
|
|
1488
|
+
'message',
|
|
1489
|
+
mediaKeySessionContext._onmessage,
|
|
1490
|
+
);
|
|
1491
|
+
mediaKeySessionContext._onmessage = undefined;
|
|
1492
|
+
}
|
|
1493
|
+
if (mediaKeySessionContext._onkeystatuseschange) {
|
|
1494
|
+
mediaKeysSession.removeEventListener(
|
|
1495
|
+
'keystatuseschange',
|
|
1496
|
+
mediaKeySessionContext._onkeystatuseschange,
|
|
1497
|
+
);
|
|
1498
|
+
mediaKeySessionContext._onkeystatuseschange = undefined;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
if (licenseXhr && licenseXhr.readyState !== XMLHttpRequest.DONE) {
|
|
1502
|
+
licenseXhr.abort();
|
|
1503
|
+
}
|
|
1504
|
+
mediaKeySessionContext.mediaKeysSession =
|
|
1505
|
+
mediaKeySessionContext.decryptdata =
|
|
1506
|
+
mediaKeySessionContext.licenseXhr =
|
|
1507
|
+
undefined!;
|
|
1508
|
+
const index = this.mediaKeySessions.indexOf(mediaKeySessionContext);
|
|
1509
|
+
if (index > -1) {
|
|
1510
|
+
this.mediaKeySessions.splice(index, 1);
|
|
1511
|
+
}
|
|
1512
|
+
const { keyStatusTimeouts } = mediaKeySessionContext;
|
|
1513
|
+
if (keyStatusTimeouts) {
|
|
1514
|
+
Object.keys(keyStatusTimeouts).forEach((keyId) =>
|
|
1515
|
+
self.clearTimeout(keyStatusTimeouts[keyId]),
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
const { drmSystemOptions } = this.config;
|
|
1519
|
+
const removePromise = isPersistentSessionType(drmSystemOptions)
|
|
1520
|
+
? new Promise((resolve, reject) => {
|
|
1521
|
+
self.setTimeout(
|
|
1522
|
+
() => reject(new Error(`MediaKeySession.remove() timeout`)),
|
|
1523
|
+
8000,
|
|
1524
|
+
);
|
|
1525
|
+
mediaKeysSession.remove().then(resolve).catch(reject);
|
|
1526
|
+
})
|
|
1527
|
+
: Promise.resolve();
|
|
1528
|
+
return removePromise
|
|
1529
|
+
.catch((error) => {
|
|
1530
|
+
this.log(`Could not remove session: ${error}`);
|
|
1531
|
+
if (!this.hls as any) return;
|
|
1532
|
+
this.hls.trigger(Events.ERROR, {
|
|
1533
|
+
type: ErrorTypes.OTHER_ERROR,
|
|
1534
|
+
details: ErrorDetails.KEY_SYSTEM_DESTROY_REMOVE_SESSION_ERROR,
|
|
1535
|
+
fatal: false,
|
|
1536
|
+
error: new Error(`Could not remove session: ${error}`),
|
|
1537
|
+
});
|
|
1538
|
+
})
|
|
1539
|
+
.then(() => {
|
|
1540
|
+
return mediaKeysSession.close();
|
|
1541
|
+
})
|
|
1542
|
+
.catch((error) => {
|
|
1543
|
+
this.log(`Could not close session: ${error}`);
|
|
1544
|
+
if (!this.hls as any) return;
|
|
1545
|
+
this.hls.trigger(Events.ERROR, {
|
|
1546
|
+
type: ErrorTypes.OTHER_ERROR,
|
|
1547
|
+
details: ErrorDetails.KEY_SYSTEM_DESTROY_CLOSE_SESSION_ERROR,
|
|
1548
|
+
fatal: false,
|
|
1549
|
+
error: new Error(`Could not close session: ${error}`),
|
|
1550
|
+
});
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
return Promise.resolve();
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function getKeyIdString(decryptdata: DecryptData | undefined): string | never {
|
|
1558
|
+
if (!decryptdata) {
|
|
1559
|
+
throw new Error('Could not read keyId of undefined decryptdata');
|
|
1560
|
+
}
|
|
1561
|
+
if (decryptdata.keyId === null) {
|
|
1562
|
+
throw new Error('keyId is null');
|
|
1563
|
+
}
|
|
1564
|
+
return arrayToHex(decryptdata.keyId);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function getKeyStatus(
|
|
1568
|
+
decryptdata: LevelKey,
|
|
1569
|
+
keyContext: MediaKeySessionContext,
|
|
1570
|
+
): MediaKeyStatus | undefined {
|
|
1571
|
+
if (
|
|
1572
|
+
decryptdata.keyId &&
|
|
1573
|
+
keyContext.mediaKeysSession.keyStatuses.has(decryptdata.keyId)
|
|
1574
|
+
) {
|
|
1575
|
+
return keyContext.mediaKeysSession.keyStatuses.get(decryptdata.keyId);
|
|
1576
|
+
}
|
|
1577
|
+
if (decryptdata.matches(keyContext.decryptdata)) {
|
|
1578
|
+
return keyContext.keyStatus;
|
|
1579
|
+
}
|
|
1580
|
+
return undefined;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
export class EMEKeyError extends Error {
|
|
1584
|
+
public readonly data: ErrorData;
|
|
1585
|
+
constructor(
|
|
1586
|
+
data: Omit<ErrorData, 'error'> & { error?: Error },
|
|
1587
|
+
message: string,
|
|
1588
|
+
) {
|
|
1589
|
+
super(message);
|
|
1590
|
+
data.error ||= new Error(message);
|
|
1591
|
+
this.data = data as ErrorData;
|
|
1592
|
+
data.err = data.error;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function getKeyStatusError(
|
|
1597
|
+
keyStatus: MediaKeyStatus,
|
|
1598
|
+
decryptdata: LevelKey,
|
|
1599
|
+
): EMEKeyError {
|
|
1600
|
+
const outputRestricted = keyStatus === 'output-restricted';
|
|
1601
|
+
const details = outputRestricted
|
|
1602
|
+
? ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED
|
|
1603
|
+
: ErrorDetails.KEY_SYSTEM_STATUS_INTERNAL_ERROR;
|
|
1604
|
+
return new EMEKeyError(
|
|
1605
|
+
{
|
|
1606
|
+
type: ErrorTypes.KEY_SYSTEM_ERROR,
|
|
1607
|
+
details,
|
|
1608
|
+
fatal: false,
|
|
1609
|
+
decryptdata,
|
|
1610
|
+
},
|
|
1611
|
+
outputRestricted
|
|
1612
|
+
? 'HDCP level output restricted'
|
|
1613
|
+
: `key status changed to "${keyStatus}"`,
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
export default EMEController;
|