@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,439 @@
|
|
|
1
|
+
import { LoadError } from './fragment-loader';
|
|
2
|
+
import { LevelKey } from './level-key';
|
|
3
|
+
import { ErrorDetails, ErrorTypes } from '../errors';
|
|
4
|
+
import { type Fragment, isMediaFragment } from '../loader/fragment';
|
|
5
|
+
import { arrayToHex } from '../utils/hex';
|
|
6
|
+
import { Logger } from '../utils/logger';
|
|
7
|
+
import {
|
|
8
|
+
getKeySystemsForConfig,
|
|
9
|
+
keySystemFormatToKeySystemDomain,
|
|
10
|
+
} from '../utils/mediakeys-helper';
|
|
11
|
+
import { KeySystemFormats } from '../utils/mediakeys-helper';
|
|
12
|
+
import { parseKeyIdsFromTenc } from '../utils/mp4-tools';
|
|
13
|
+
import type { HlsConfig } from '../config';
|
|
14
|
+
import type EMEController from '../controller/eme-controller';
|
|
15
|
+
import type {
|
|
16
|
+
EMEKeyError,
|
|
17
|
+
MediaKeySessionContext,
|
|
18
|
+
} from '../controller/eme-controller';
|
|
19
|
+
import type { ComponentAPI } from '../types/component-api';
|
|
20
|
+
import type { KeyLoadedData } from '../types/events';
|
|
21
|
+
import type {
|
|
22
|
+
KeyLoaderContext,
|
|
23
|
+
Loader,
|
|
24
|
+
LoaderCallbacks,
|
|
25
|
+
LoaderConfiguration,
|
|
26
|
+
LoaderResponse,
|
|
27
|
+
LoaderStats,
|
|
28
|
+
PlaylistLevelType,
|
|
29
|
+
} from '../types/loader';
|
|
30
|
+
import type { NullableNetworkDetails } from '../types/network-details';
|
|
31
|
+
import type { ILogger } from '../utils/logger';
|
|
32
|
+
|
|
33
|
+
export interface KeyLoaderInfo {
|
|
34
|
+
decryptdata: LevelKey;
|
|
35
|
+
keyLoadPromise: Promise<KeyLoadedData> | null;
|
|
36
|
+
loader: Loader<KeyLoaderContext> | null;
|
|
37
|
+
mediaKeySessionContext: MediaKeySessionContext | null;
|
|
38
|
+
}
|
|
39
|
+
export default class KeyLoader extends Logger implements ComponentAPI {
|
|
40
|
+
private readonly config: HlsConfig;
|
|
41
|
+
private keyIdToKeyInfo: { [keyId: string]: KeyLoaderInfo | undefined } = {};
|
|
42
|
+
public emeController: EMEController | null = null;
|
|
43
|
+
|
|
44
|
+
constructor(config: HlsConfig, logger: ILogger) {
|
|
45
|
+
super('key-loader', logger);
|
|
46
|
+
this.config = config;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
abort(type?: PlaylistLevelType) {
|
|
50
|
+
for (const id in this.keyIdToKeyInfo) {
|
|
51
|
+
const loader = this.keyIdToKeyInfo[id]!.loader;
|
|
52
|
+
if (loader) {
|
|
53
|
+
if (type && type !== loader.context?.frag.type) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
loader.abort();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
detach() {
|
|
62
|
+
for (const id in this.keyIdToKeyInfo) {
|
|
63
|
+
const keyInfo = this.keyIdToKeyInfo[id]!;
|
|
64
|
+
// Remove cached EME keys on detach
|
|
65
|
+
if (
|
|
66
|
+
keyInfo.mediaKeySessionContext ||
|
|
67
|
+
keyInfo.decryptdata.isCommonEncryption
|
|
68
|
+
) {
|
|
69
|
+
delete this.keyIdToKeyInfo[id];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
destroy() {
|
|
75
|
+
this.detach();
|
|
76
|
+
for (const id in this.keyIdToKeyInfo) {
|
|
77
|
+
const loader = this.keyIdToKeyInfo[id]!.loader;
|
|
78
|
+
if (loader) {
|
|
79
|
+
loader.destroy();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
this.keyIdToKeyInfo = {};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
createKeyLoadError(
|
|
86
|
+
frag: Fragment,
|
|
87
|
+
details: ErrorDetails = ErrorDetails.KEY_LOAD_ERROR,
|
|
88
|
+
error: Error,
|
|
89
|
+
networkDetails?: NullableNetworkDetails,
|
|
90
|
+
response?: { url: string; data: undefined; code: number; text: string },
|
|
91
|
+
): LoadError {
|
|
92
|
+
return new LoadError({
|
|
93
|
+
type: ErrorTypes.NETWORK_ERROR,
|
|
94
|
+
details,
|
|
95
|
+
fatal: false,
|
|
96
|
+
frag,
|
|
97
|
+
response,
|
|
98
|
+
error,
|
|
99
|
+
networkDetails: networkDetails || null,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
loadClear(
|
|
104
|
+
loadingFrag: Fragment,
|
|
105
|
+
encryptedFragments: Fragment[],
|
|
106
|
+
startFragRequested: boolean,
|
|
107
|
+
): null | Promise<void> {
|
|
108
|
+
if (
|
|
109
|
+
__USE_EME_DRM__ &&
|
|
110
|
+
this.emeController &&
|
|
111
|
+
this.config.emeEnabled &&
|
|
112
|
+
!this.emeController.getSelectedKeySystemFormats().length
|
|
113
|
+
) {
|
|
114
|
+
// Access key-system with nearest key on start (loading frag is unencrypted)
|
|
115
|
+
if (encryptedFragments.length) {
|
|
116
|
+
for (let i = 0, l = encryptedFragments.length; i < l; i++) {
|
|
117
|
+
const frag = encryptedFragments[i];
|
|
118
|
+
// Loading at or before segment with EXT-X-KEY, or first frag loading and last EXT-X-KEY
|
|
119
|
+
if (
|
|
120
|
+
(loadingFrag.cc <= frag.cc &&
|
|
121
|
+
(!isMediaFragment(loadingFrag) ||
|
|
122
|
+
!isMediaFragment(frag) ||
|
|
123
|
+
loadingFrag.sn < frag.sn)) ||
|
|
124
|
+
(!startFragRequested && i == l - 1)
|
|
125
|
+
) {
|
|
126
|
+
return this.emeController
|
|
127
|
+
.selectKeySystemFormat(frag)
|
|
128
|
+
.then((keySystemFormat) => {
|
|
129
|
+
if (!this.emeController) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
frag.setKeyFormat(keySystemFormat);
|
|
133
|
+
const keySystem =
|
|
134
|
+
keySystemFormatToKeySystemDomain(keySystemFormat);
|
|
135
|
+
if (keySystem) {
|
|
136
|
+
return this.emeController.getKeySystemAccess([keySystem]);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (this.config.requireKeySystemAccessOnStart) {
|
|
143
|
+
const keySystemsInConfig = getKeySystemsForConfig(this.config);
|
|
144
|
+
if (keySystemsInConfig.length) {
|
|
145
|
+
return this.emeController.getKeySystemAccess(keySystemsInConfig);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
load(frag: Fragment): Promise<KeyLoadedData> {
|
|
153
|
+
if (
|
|
154
|
+
!frag.decryptdata &&
|
|
155
|
+
frag.encrypted &&
|
|
156
|
+
this.emeController &&
|
|
157
|
+
this.config.emeEnabled
|
|
158
|
+
) {
|
|
159
|
+
// Multiple keys, but none selected, resolve in eme-controller
|
|
160
|
+
return this.emeController
|
|
161
|
+
.selectKeySystemFormat(frag)
|
|
162
|
+
.then((keySystemFormat) => {
|
|
163
|
+
return this.loadInternal(frag, keySystemFormat);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return this.loadInternal(frag);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
loadInternal(
|
|
171
|
+
frag: Fragment,
|
|
172
|
+
keySystemFormat?: KeySystemFormats,
|
|
173
|
+
): Promise<KeyLoadedData> {
|
|
174
|
+
if (__USE_EME_DRM__ && keySystemFormat) {
|
|
175
|
+
frag.setKeyFormat(keySystemFormat);
|
|
176
|
+
}
|
|
177
|
+
const decryptdata = frag.decryptdata;
|
|
178
|
+
if (!decryptdata) {
|
|
179
|
+
const error = new Error(
|
|
180
|
+
keySystemFormat
|
|
181
|
+
? `Expected frag.decryptdata to be defined after setting format ${keySystemFormat}`
|
|
182
|
+
: `Missing decryption data on fragment in onKeyLoading (emeEnabled with controller: ${this.emeController && this.config.emeEnabled})`,
|
|
183
|
+
);
|
|
184
|
+
return Promise.reject(
|
|
185
|
+
this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, error),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const uri = decryptdata.uri;
|
|
189
|
+
if (!uri) {
|
|
190
|
+
return Promise.reject(
|
|
191
|
+
this.createKeyLoadError(
|
|
192
|
+
frag,
|
|
193
|
+
ErrorDetails.KEY_LOAD_ERROR,
|
|
194
|
+
new Error(`Invalid key URI: "${uri}"`),
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const id = getKeyId(decryptdata);
|
|
199
|
+
let keyInfo = this.keyIdToKeyInfo[id];
|
|
200
|
+
|
|
201
|
+
if (keyInfo?.decryptdata.key) {
|
|
202
|
+
decryptdata.key = keyInfo.decryptdata.key;
|
|
203
|
+
return Promise.resolve({ frag, keyInfo });
|
|
204
|
+
}
|
|
205
|
+
// Return key load promise once it has a mediakey session with an usable key status
|
|
206
|
+
if (this.emeController && keyInfo?.keyLoadPromise) {
|
|
207
|
+
const keyStatus = this.emeController.getKeyStatus(keyInfo.decryptdata);
|
|
208
|
+
switch (keyStatus) {
|
|
209
|
+
case 'usable':
|
|
210
|
+
case 'usable-in-future':
|
|
211
|
+
return keyInfo.keyLoadPromise.then((keyLoadedData) => {
|
|
212
|
+
// Return the correct fragment with updated decryptdata key and loaded keyInfo
|
|
213
|
+
const { keyInfo } = keyLoadedData;
|
|
214
|
+
decryptdata.key = keyInfo.decryptdata.key;
|
|
215
|
+
return { frag, keyInfo };
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
// If we have a key session and status and it is not pending or usable, continue
|
|
219
|
+
// This will go back to the eme-controller for expired keys to get a new keyLoadPromise
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Load the key or return the loading promise
|
|
223
|
+
this.log(
|
|
224
|
+
`${this.keyIdToKeyInfo[id] ? 'Rel' : 'L'}oading${decryptdata.keyId ? ' keyId: ' + arrayToHex(decryptdata.keyId) : ''} URI: ${decryptdata.uri} from ${frag.type} ${frag.level}`,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
keyInfo = this.keyIdToKeyInfo[id] = {
|
|
228
|
+
decryptdata,
|
|
229
|
+
keyLoadPromise: null,
|
|
230
|
+
loader: null,
|
|
231
|
+
mediaKeySessionContext: null,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
switch (decryptdata.method) {
|
|
235
|
+
case 'SAMPLE-AES':
|
|
236
|
+
case 'SAMPLE-AES-CENC':
|
|
237
|
+
case 'SAMPLE-AES-CTR':
|
|
238
|
+
if (decryptdata.keyFormat === 'identity') {
|
|
239
|
+
// loadKeyHTTP handles http(s) and data URLs
|
|
240
|
+
return this.loadKeyHTTP(keyInfo, frag);
|
|
241
|
+
}
|
|
242
|
+
return this.loadKeyEME(keyInfo, frag);
|
|
243
|
+
case 'AES-128':
|
|
244
|
+
case 'AES-256':
|
|
245
|
+
case 'AES-256-CTR':
|
|
246
|
+
return this.loadKeyHTTP(keyInfo, frag);
|
|
247
|
+
default:
|
|
248
|
+
return Promise.reject(
|
|
249
|
+
this.createKeyLoadError(
|
|
250
|
+
frag,
|
|
251
|
+
ErrorDetails.KEY_LOAD_ERROR,
|
|
252
|
+
new Error(
|
|
253
|
+
`Key supplied with unsupported METHOD: "${decryptdata.method}"`,
|
|
254
|
+
),
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
loadKeyEME(keyInfo: KeyLoaderInfo, frag: Fragment): Promise<KeyLoadedData> {
|
|
261
|
+
const keyLoadedData: KeyLoadedData = { frag, keyInfo };
|
|
262
|
+
if (this.emeController && this.config.emeEnabled) {
|
|
263
|
+
if (!keyInfo.decryptdata.keyId && frag.initSegment?.data) {
|
|
264
|
+
const keyIds = parseKeyIdsFromTenc(
|
|
265
|
+
frag.initSegment.data as Uint8Array<ArrayBuffer>,
|
|
266
|
+
);
|
|
267
|
+
if (keyIds.length) {
|
|
268
|
+
let keyId = keyIds[0];
|
|
269
|
+
if (keyId.some((b) => b !== 0)) {
|
|
270
|
+
this.log(`Using keyId found in init segment ${arrayToHex(keyId)}`);
|
|
271
|
+
LevelKey.setKeyIdForUri(keyInfo.decryptdata.uri, keyId);
|
|
272
|
+
} else {
|
|
273
|
+
keyId = LevelKey.addKeyIdForUri(keyInfo.decryptdata.uri);
|
|
274
|
+
this.log(`Generating keyId to patch media ${arrayToHex(keyId)}`);
|
|
275
|
+
}
|
|
276
|
+
keyInfo.decryptdata.keyId = keyId;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!keyInfo.decryptdata.keyId && !isMediaFragment(frag)) {
|
|
280
|
+
// Resolve so that unencrypted init segment is loaded
|
|
281
|
+
// key id is extracted from tenc box when processing key for next segment above
|
|
282
|
+
return Promise.resolve(keyLoadedData);
|
|
283
|
+
}
|
|
284
|
+
const keySessionContextPromise =
|
|
285
|
+
this.emeController.loadKey(keyLoadedData);
|
|
286
|
+
return (keyInfo.keyLoadPromise = keySessionContextPromise.then(
|
|
287
|
+
(keySessionContext) => {
|
|
288
|
+
keyInfo.mediaKeySessionContext = keySessionContext;
|
|
289
|
+
return keyLoadedData;
|
|
290
|
+
},
|
|
291
|
+
)).catch((error: EMEKeyError | Error) => {
|
|
292
|
+
// Remove promise for license renewal or retry
|
|
293
|
+
keyInfo.keyLoadPromise = null;
|
|
294
|
+
if ('data' in error) {
|
|
295
|
+
error.data.frag = frag;
|
|
296
|
+
}
|
|
297
|
+
throw error;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return Promise.resolve(keyLoadedData);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
loadKeyHTTP(keyInfo: KeyLoaderInfo, frag: Fragment): Promise<KeyLoadedData> {
|
|
304
|
+
const config = this.config;
|
|
305
|
+
const Loader = config.loader;
|
|
306
|
+
const keyLoader = new Loader(config) as Loader<KeyLoaderContext>;
|
|
307
|
+
frag.keyLoader = keyInfo.loader = keyLoader;
|
|
308
|
+
|
|
309
|
+
return (keyInfo.keyLoadPromise = new Promise((resolve, reject) => {
|
|
310
|
+
const loaderContext: KeyLoaderContext = {
|
|
311
|
+
keyInfo,
|
|
312
|
+
frag,
|
|
313
|
+
responseType: 'arraybuffer',
|
|
314
|
+
url: keyInfo.decryptdata.uri,
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// maxRetry is 0 so that instead of retrying the same key on the same variant multiple times,
|
|
318
|
+
// key-loader will trigger an error and rely on stream-controller to handle retry logic.
|
|
319
|
+
// this will also align retry logic with fragment-loader
|
|
320
|
+
const loadPolicy = config.keyLoadPolicy.default;
|
|
321
|
+
const loaderConfig: LoaderConfiguration = {
|
|
322
|
+
loadPolicy,
|
|
323
|
+
timeout: loadPolicy.maxLoadTimeMs,
|
|
324
|
+
maxRetry: 0,
|
|
325
|
+
retryDelay: 0,
|
|
326
|
+
maxRetryDelay: 0,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const loaderCallbacks: LoaderCallbacks<KeyLoaderContext> = {
|
|
330
|
+
onSuccess: (
|
|
331
|
+
response: LoaderResponse,
|
|
332
|
+
stats: LoaderStats,
|
|
333
|
+
context: KeyLoaderContext,
|
|
334
|
+
networkDetails: NullableNetworkDetails,
|
|
335
|
+
) => {
|
|
336
|
+
const { frag, keyInfo } = context;
|
|
337
|
+
const id = getKeyId(keyInfo.decryptdata);
|
|
338
|
+
if (!frag.decryptdata || keyInfo !== this.keyIdToKeyInfo[id]) {
|
|
339
|
+
return reject(
|
|
340
|
+
this.createKeyLoadError(
|
|
341
|
+
frag,
|
|
342
|
+
ErrorDetails.KEY_LOAD_ERROR,
|
|
343
|
+
new Error('after key load, decryptdata unset or changed'),
|
|
344
|
+
networkDetails,
|
|
345
|
+
),
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
keyInfo.decryptdata.key = frag.decryptdata.key = new Uint8Array(
|
|
350
|
+
response.data as ArrayBuffer,
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// detach fragment key loader on load success
|
|
354
|
+
frag.keyLoader = null;
|
|
355
|
+
keyInfo.loader = null;
|
|
356
|
+
resolve({ frag, keyInfo });
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
onError: (
|
|
360
|
+
response: { code: number; text: string },
|
|
361
|
+
context: KeyLoaderContext,
|
|
362
|
+
networkDetails: NullableNetworkDetails,
|
|
363
|
+
stats: LoaderStats,
|
|
364
|
+
) => {
|
|
365
|
+
this.resetLoader(context);
|
|
366
|
+
reject(
|
|
367
|
+
this.createKeyLoadError(
|
|
368
|
+
frag,
|
|
369
|
+
ErrorDetails.KEY_LOAD_ERROR,
|
|
370
|
+
new Error(
|
|
371
|
+
`HTTP Error ${response.code} loading key ${response.text}`,
|
|
372
|
+
),
|
|
373
|
+
networkDetails,
|
|
374
|
+
{ url: loaderContext.url, data: undefined, ...response },
|
|
375
|
+
),
|
|
376
|
+
);
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
onTimeout: (
|
|
380
|
+
stats: LoaderStats,
|
|
381
|
+
context: KeyLoaderContext,
|
|
382
|
+
networkDetails: NullableNetworkDetails,
|
|
383
|
+
) => {
|
|
384
|
+
this.resetLoader(context);
|
|
385
|
+
reject(
|
|
386
|
+
this.createKeyLoadError(
|
|
387
|
+
frag,
|
|
388
|
+
ErrorDetails.KEY_LOAD_TIMEOUT,
|
|
389
|
+
new Error('key loading timed out'),
|
|
390
|
+
networkDetails,
|
|
391
|
+
),
|
|
392
|
+
);
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
onAbort: (
|
|
396
|
+
stats: LoaderStats,
|
|
397
|
+
context: KeyLoaderContext,
|
|
398
|
+
networkDetails: NullableNetworkDetails,
|
|
399
|
+
) => {
|
|
400
|
+
this.resetLoader(context);
|
|
401
|
+
reject(
|
|
402
|
+
this.createKeyLoadError(
|
|
403
|
+
frag,
|
|
404
|
+
ErrorDetails.INTERNAL_ABORTED,
|
|
405
|
+
new Error('key loading aborted'),
|
|
406
|
+
networkDetails,
|
|
407
|
+
),
|
|
408
|
+
);
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
keyLoader.load(loaderContext, loaderConfig, loaderCallbacks);
|
|
413
|
+
}));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private resetLoader(context: KeyLoaderContext) {
|
|
417
|
+
const { frag, keyInfo, url: uri } = context;
|
|
418
|
+
const loader = keyInfo.loader;
|
|
419
|
+
if (frag.keyLoader === loader) {
|
|
420
|
+
frag.keyLoader = null;
|
|
421
|
+
keyInfo.loader = null;
|
|
422
|
+
}
|
|
423
|
+
const id = getKeyId(keyInfo.decryptdata) || uri;
|
|
424
|
+
delete this.keyIdToKeyInfo[id];
|
|
425
|
+
if (loader) {
|
|
426
|
+
loader.destroy();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function getKeyId(decryptdata: LevelKey) {
|
|
432
|
+
if (__USE_EME_DRM__ && decryptdata.keyFormat !== KeySystemFormats.FAIRPLAY) {
|
|
433
|
+
const keyId = decryptdata.keyId;
|
|
434
|
+
if (keyId) {
|
|
435
|
+
return arrayToHex(keyId);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return decryptdata.uri;
|
|
439
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { DateRange } from './date-range';
|
|
2
|
+
import type { Fragment, MediaFragment, Part } from './fragment';
|
|
3
|
+
import type { LevelKey } from './level-key';
|
|
4
|
+
import type { VariableMap } from '../types/level';
|
|
5
|
+
import type { AttrList } from '../utils/attr-list';
|
|
6
|
+
import type { KeySystemFormats } from '../utils/mediakeys-helper';
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_TARGET_DURATION = 10;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Object representing parsed data from an HLS Media Playlist. Found in {@link hls.js#Level.details}.
|
|
12
|
+
*/
|
|
13
|
+
export class LevelDetails {
|
|
14
|
+
public PTSKnown: boolean = false;
|
|
15
|
+
public alignedSliding: boolean = false;
|
|
16
|
+
public averagetargetduration?: number;
|
|
17
|
+
public endCC: number = 0;
|
|
18
|
+
public endSN: number = 0;
|
|
19
|
+
public fragments: MediaFragment[];
|
|
20
|
+
public fragmentHint?: MediaFragment;
|
|
21
|
+
public partList: Part[] | null = null;
|
|
22
|
+
public dateRanges: Record<string, DateRange | undefined>;
|
|
23
|
+
public dateRangeTagCount: number = 0;
|
|
24
|
+
public live: boolean = true;
|
|
25
|
+
public requestScheduled: number = -1;
|
|
26
|
+
public ageHeader: number = 0;
|
|
27
|
+
public advancedDateTime?: number;
|
|
28
|
+
public updated: boolean = true;
|
|
29
|
+
public advanced: boolean = true;
|
|
30
|
+
public misses: number = 0;
|
|
31
|
+
public startCC: number = 0;
|
|
32
|
+
public startSN: number = 0;
|
|
33
|
+
public startTimeOffset: number | null = null;
|
|
34
|
+
public targetduration: number = 0;
|
|
35
|
+
public totalduration: number = 0;
|
|
36
|
+
public type: string | null = null;
|
|
37
|
+
public url: string;
|
|
38
|
+
public m3u8: string = '';
|
|
39
|
+
public version: number | null = null;
|
|
40
|
+
public canBlockReload: boolean = false;
|
|
41
|
+
public canSkipUntil: number = 0;
|
|
42
|
+
public canSkipDateRanges: boolean = false;
|
|
43
|
+
public skippedSegments: number = 0;
|
|
44
|
+
public recentlyRemovedDateranges?: string[];
|
|
45
|
+
public partHoldBack: number = 0;
|
|
46
|
+
public holdBack: number = 0;
|
|
47
|
+
public partTarget: number = 0;
|
|
48
|
+
public preloadHint?: AttrList;
|
|
49
|
+
public renditionReports?: AttrList[];
|
|
50
|
+
public tuneInGoal: number = 0;
|
|
51
|
+
public deltaUpdateFailed?: boolean;
|
|
52
|
+
public driftStartTime: number = 0;
|
|
53
|
+
public driftEndTime: number = 0;
|
|
54
|
+
public driftStart: number = 0;
|
|
55
|
+
public driftEnd: number = 0;
|
|
56
|
+
public encryptedFragments: Fragment[];
|
|
57
|
+
public playlistParsingError: Error | null = null;
|
|
58
|
+
public variableList: VariableMap | null = null;
|
|
59
|
+
public hasVariableRefs = false;
|
|
60
|
+
public appliedTimelineOffset?: number;
|
|
61
|
+
|
|
62
|
+
constructor(baseUrl: string) {
|
|
63
|
+
this.fragments = [];
|
|
64
|
+
this.encryptedFragments = [];
|
|
65
|
+
this.dateRanges = {};
|
|
66
|
+
this.url = baseUrl;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
reloaded(previous: LevelDetails | undefined) {
|
|
70
|
+
if (!previous) {
|
|
71
|
+
this.advanced = true;
|
|
72
|
+
this.updated = true;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const partSnDiff = this.lastPartSn - previous.lastPartSn;
|
|
76
|
+
const partIndexDiff = this.lastPartIndex - previous.lastPartIndex;
|
|
77
|
+
this.updated =
|
|
78
|
+
this.endSN !== previous.endSN ||
|
|
79
|
+
!!partIndexDiff ||
|
|
80
|
+
!!partSnDiff ||
|
|
81
|
+
!this.live;
|
|
82
|
+
this.advanced =
|
|
83
|
+
this.endSN > previous.endSN ||
|
|
84
|
+
partSnDiff > 0 ||
|
|
85
|
+
(partSnDiff === 0 && partIndexDiff > 0);
|
|
86
|
+
if (this.updated || this.advanced) {
|
|
87
|
+
this.misses = 0;
|
|
88
|
+
} else {
|
|
89
|
+
this.misses = previous.misses + 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
hasKey(levelKey: LevelKey): boolean {
|
|
94
|
+
return this.encryptedFragments.some((frag) => {
|
|
95
|
+
let decryptdata = frag.decryptdata;
|
|
96
|
+
if (!decryptdata) {
|
|
97
|
+
frag.setKeyFormat(levelKey.keyFormat as KeySystemFormats);
|
|
98
|
+
decryptdata = frag.decryptdata;
|
|
99
|
+
}
|
|
100
|
+
return !!decryptdata && levelKey.matches(decryptdata);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get hasProgramDateTime(): boolean {
|
|
105
|
+
if (this.fragments.length) {
|
|
106
|
+
return Number.isFinite(
|
|
107
|
+
this.fragments[this.fragments.length - 1].programDateTime,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get levelTargetDuration(): number {
|
|
114
|
+
return (
|
|
115
|
+
this.averagetargetduration ||
|
|
116
|
+
this.targetduration ||
|
|
117
|
+
DEFAULT_TARGET_DURATION
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get drift(): number {
|
|
122
|
+
const runTime = this.driftEndTime - this.driftStartTime;
|
|
123
|
+
if (runTime > 0) {
|
|
124
|
+
const runDuration = this.driftEnd - this.driftStart;
|
|
125
|
+
return (runDuration * 1000) / runTime;
|
|
126
|
+
}
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get edge(): number {
|
|
131
|
+
return this.partEnd || this.fragmentEnd;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get partEnd(): number {
|
|
135
|
+
if (this.partList?.length) {
|
|
136
|
+
return this.partList[this.partList.length - 1].end;
|
|
137
|
+
}
|
|
138
|
+
return this.fragmentEnd;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get fragmentEnd(): number {
|
|
142
|
+
if (this.fragments.length) {
|
|
143
|
+
return this.fragments[this.fragments.length - 1].end;
|
|
144
|
+
}
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get fragmentStart(): number {
|
|
149
|
+
if (this.fragments.length) {
|
|
150
|
+
return this.fragments[0].start;
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get age(): number {
|
|
156
|
+
if (this.advancedDateTime) {
|
|
157
|
+
return Math.max(Date.now() - this.advancedDateTime, 0) / 1000;
|
|
158
|
+
}
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get lastPartIndex(): number {
|
|
163
|
+
if (this.partList?.length) {
|
|
164
|
+
return this.partList[this.partList.length - 1].index;
|
|
165
|
+
}
|
|
166
|
+
return -1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
get maxPartIndex(): number {
|
|
170
|
+
const partList = this.partList;
|
|
171
|
+
if (partList) {
|
|
172
|
+
const lastIndex = this.lastPartIndex;
|
|
173
|
+
if (lastIndex !== -1) {
|
|
174
|
+
for (let i = partList.length; i--; ) {
|
|
175
|
+
if (partList[i].index > lastIndex) {
|
|
176
|
+
return partList[i].index;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return lastIndex;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
get lastPartSn(): number {
|
|
186
|
+
if (this.partList?.length) {
|
|
187
|
+
return this.partList[this.partList.length - 1].fragment.sn;
|
|
188
|
+
}
|
|
189
|
+
return this.endSN;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
get expired(): boolean {
|
|
193
|
+
if (this.live && this.age && this.misses < 3) {
|
|
194
|
+
const playlistWindowDuration = this.partEnd - this.fragmentStart;
|
|
195
|
+
return (
|
|
196
|
+
this.age >
|
|
197
|
+
Math.max(playlistWindowDuration, this.totalduration) +
|
|
198
|
+
this.levelTargetDuration
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|