@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.
Files changed (159) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +472 -0
  3. package/dist/hls-demo.js +26995 -0
  4. package/dist/hls-demo.js.map +1 -0
  5. package/dist/hls.d.mts +4204 -0
  6. package/dist/hls.d.ts +4204 -0
  7. package/dist/hls.js +40050 -0
  8. package/dist/hls.js.d.ts +4204 -0
  9. package/dist/hls.js.map +1 -0
  10. package/dist/hls.light.js +27145 -0
  11. package/dist/hls.light.js.map +1 -0
  12. package/dist/hls.light.min.js +2 -0
  13. package/dist/hls.light.min.js.map +1 -0
  14. package/dist/hls.light.mjs +26392 -0
  15. package/dist/hls.light.mjs.map +1 -0
  16. package/dist/hls.min.js +2 -0
  17. package/dist/hls.min.js.map +1 -0
  18. package/dist/hls.mjs +38956 -0
  19. package/dist/hls.mjs.map +1 -0
  20. package/dist/hls.worker.js +2 -0
  21. package/dist/hls.worker.js.map +1 -0
  22. package/package.json +143 -0
  23. package/src/config.ts +794 -0
  24. package/src/controller/abr-controller.ts +1019 -0
  25. package/src/controller/algo-data-controller.ts +794 -0
  26. package/src/controller/audio-stream-controller.ts +1099 -0
  27. package/src/controller/audio-track-controller.ts +454 -0
  28. package/src/controller/base-playlist-controller.ts +438 -0
  29. package/src/controller/base-stream-controller.ts +2526 -0
  30. package/src/controller/buffer-controller.ts +2015 -0
  31. package/src/controller/buffer-operation-queue.ts +159 -0
  32. package/src/controller/cap-level-controller.ts +367 -0
  33. package/src/controller/cmcd-controller.ts +422 -0
  34. package/src/controller/content-steering-controller.ts +622 -0
  35. package/src/controller/eme-controller.ts +1617 -0
  36. package/src/controller/error-controller.ts +627 -0
  37. package/src/controller/fps-controller.ts +146 -0
  38. package/src/controller/fragment-finders.ts +256 -0
  39. package/src/controller/fragment-tracker.ts +567 -0
  40. package/src/controller/gap-controller.ts +719 -0
  41. package/src/controller/id3-track-controller.ts +488 -0
  42. package/src/controller/interstitial-player.ts +302 -0
  43. package/src/controller/interstitials-controller.ts +2895 -0
  44. package/src/controller/interstitials-schedule.ts +698 -0
  45. package/src/controller/latency-controller.ts +294 -0
  46. package/src/controller/level-controller.ts +776 -0
  47. package/src/controller/stream-controller.ts +1597 -0
  48. package/src/controller/subtitle-stream-controller.ts +508 -0
  49. package/src/controller/subtitle-track-controller.ts +617 -0
  50. package/src/controller/timeline-controller.ts +677 -0
  51. package/src/crypt/aes-crypto.ts +36 -0
  52. package/src/crypt/aes-decryptor.ts +339 -0
  53. package/src/crypt/decrypter-aes-mode.ts +4 -0
  54. package/src/crypt/decrypter.ts +225 -0
  55. package/src/crypt/fast-aes-key.ts +39 -0
  56. package/src/define-plugin.d.ts +17 -0
  57. package/src/demux/audio/aacdemuxer.ts +126 -0
  58. package/src/demux/audio/ac3-demuxer.ts +170 -0
  59. package/src/demux/audio/adts.ts +249 -0
  60. package/src/demux/audio/base-audio-demuxer.ts +205 -0
  61. package/src/demux/audio/dolby.ts +21 -0
  62. package/src/demux/audio/mp3demuxer.ts +85 -0
  63. package/src/demux/audio/mpegaudio.ts +177 -0
  64. package/src/demux/chunk-cache.ts +42 -0
  65. package/src/demux/dummy-demuxed-track.ts +13 -0
  66. package/src/demux/inject-worker.ts +75 -0
  67. package/src/demux/mp4demuxer.ts +234 -0
  68. package/src/demux/sample-aes.ts +198 -0
  69. package/src/demux/transmuxer-interface.ts +449 -0
  70. package/src/demux/transmuxer-worker.ts +221 -0
  71. package/src/demux/transmuxer.ts +560 -0
  72. package/src/demux/tsdemuxer.ts +1256 -0
  73. package/src/demux/video/avc-video-parser.ts +401 -0
  74. package/src/demux/video/base-video-parser.ts +198 -0
  75. package/src/demux/video/exp-golomb.ts +153 -0
  76. package/src/demux/video/hevc-video-parser.ts +736 -0
  77. package/src/empty-es.js +5 -0
  78. package/src/empty.js +3 -0
  79. package/src/errors.ts +107 -0
  80. package/src/events.ts +548 -0
  81. package/src/exports-default.ts +3 -0
  82. package/src/exports-named.ts +81 -0
  83. package/src/hls.ts +1613 -0
  84. package/src/is-supported.ts +54 -0
  85. package/src/loader/date-range.ts +207 -0
  86. package/src/loader/fragment-loader.ts +403 -0
  87. package/src/loader/fragment.ts +487 -0
  88. package/src/loader/interstitial-asset-list.ts +162 -0
  89. package/src/loader/interstitial-event.ts +337 -0
  90. package/src/loader/key-loader.ts +439 -0
  91. package/src/loader/level-details.ts +203 -0
  92. package/src/loader/level-key.ts +259 -0
  93. package/src/loader/load-stats.ts +17 -0
  94. package/src/loader/m3u8-parser.ts +1072 -0
  95. package/src/loader/playlist-loader.ts +839 -0
  96. package/src/polyfills/number.ts +15 -0
  97. package/src/remux/aac-helper.ts +81 -0
  98. package/src/remux/mp4-generator.ts +1380 -0
  99. package/src/remux/mp4-remuxer.ts +1261 -0
  100. package/src/remux/passthrough-remuxer.ts +434 -0
  101. package/src/task-loop.ts +130 -0
  102. package/src/types/algo.ts +44 -0
  103. package/src/types/buffer.ts +105 -0
  104. package/src/types/component-api.ts +20 -0
  105. package/src/types/demuxer.ts +208 -0
  106. package/src/types/events.ts +574 -0
  107. package/src/types/fragment-tracker.ts +23 -0
  108. package/src/types/level.ts +268 -0
  109. package/src/types/loader.ts +198 -0
  110. package/src/types/media-playlist.ts +92 -0
  111. package/src/types/network-details.ts +3 -0
  112. package/src/types/remuxer.ts +104 -0
  113. package/src/types/track.ts +12 -0
  114. package/src/types/transmuxer.ts +46 -0
  115. package/src/types/tuples.ts +6 -0
  116. package/src/types/vtt.ts +11 -0
  117. package/src/utils/arrays.ts +22 -0
  118. package/src/utils/attr-list.ts +192 -0
  119. package/src/utils/binary-search.ts +46 -0
  120. package/src/utils/buffer-helper.ts +173 -0
  121. package/src/utils/cea-608-parser.ts +1413 -0
  122. package/src/utils/chunker.ts +41 -0
  123. package/src/utils/codecs.ts +314 -0
  124. package/src/utils/cues.ts +96 -0
  125. package/src/utils/discontinuities.ts +174 -0
  126. package/src/utils/encryption-methods-util.ts +21 -0
  127. package/src/utils/error-helper.ts +95 -0
  128. package/src/utils/event-listener-helper.ts +16 -0
  129. package/src/utils/ewma-bandwidth-estimator.ts +97 -0
  130. package/src/utils/ewma.ts +43 -0
  131. package/src/utils/fetch-loader.ts +331 -0
  132. package/src/utils/global.ts +2 -0
  133. package/src/utils/hash.ts +10 -0
  134. package/src/utils/hdr.ts +67 -0
  135. package/src/utils/hex.ts +32 -0
  136. package/src/utils/imsc1-ttml-parser.ts +261 -0
  137. package/src/utils/keysystem-util.ts +45 -0
  138. package/src/utils/level-helper.ts +629 -0
  139. package/src/utils/logger.ts +120 -0
  140. package/src/utils/media-option-attributes.ts +49 -0
  141. package/src/utils/mediacapabilities-helper.ts +301 -0
  142. package/src/utils/mediakeys-helper.ts +210 -0
  143. package/src/utils/mediasource-helper.ts +37 -0
  144. package/src/utils/mp4-tools.ts +1473 -0
  145. package/src/utils/number.ts +3 -0
  146. package/src/utils/numeric-encoding-utils.ts +26 -0
  147. package/src/utils/output-filter.ts +46 -0
  148. package/src/utils/rendition-helper.ts +505 -0
  149. package/src/utils/safe-json-stringify.ts +22 -0
  150. package/src/utils/texttrack-utils.ts +164 -0
  151. package/src/utils/time-ranges.ts +17 -0
  152. package/src/utils/timescale-conversion.ts +46 -0
  153. package/src/utils/utf8-utils.ts +18 -0
  154. package/src/utils/variable-substitution.ts +105 -0
  155. package/src/utils/vttcue.ts +384 -0
  156. package/src/utils/vttparser.ts +497 -0
  157. package/src/utils/webvtt-parser.ts +166 -0
  158. package/src/utils/xhr-loader.ts +337 -0
  159. 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;