@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,839 @@
1
+ /**
2
+ * PlaylistLoader - delegate for media manifest/playlist loading tasks. Takes care of parsing media to internal data-models.
3
+ *
4
+ * Once loaded, dispatches events with parsed data-models of manifest/levels/audio/subtitle tracks.
5
+ *
6
+ * Uses loader(s) set in config to do actual internal loading of resource tasks.
7
+ */
8
+
9
+ import M3U8Parser from './m3u8-parser';
10
+ import { ErrorDetails, ErrorTypes } from '../errors';
11
+ import { Events } from '../events';
12
+ import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
13
+ import { AttrList } from '../utils/attr-list';
14
+ import {
15
+ areCodecsMediaSourceSupported,
16
+ sampleEntryCodesISO,
17
+ } from '../utils/codecs';
18
+ import { computeReloadInterval } from '../utils/level-helper';
19
+ import type { LevelDetails } from './level-details';
20
+ import type { LoaderConfig, RetryConfig } from '../config';
21
+ import type Hls from '../hls';
22
+ import type { NetworkComponentAPI } from '../types/component-api';
23
+ import type {
24
+ ErrorData,
25
+ LevelLoadingData,
26
+ LevelsUpdatedData,
27
+ ManifestLoadingData,
28
+ TrackLoadingData,
29
+ } from '../types/events';
30
+ import type { Level, LevelParsed, VariableMap } from '../types/level';
31
+ import type {
32
+ Loader,
33
+ LoaderCallbacks,
34
+ LoaderConfiguration,
35
+ LoaderContext,
36
+ LoaderResponse,
37
+ LoaderStats,
38
+ PlaylistLoaderContext,
39
+ } from '../types/loader';
40
+ import type { MediaAttributes, MediaPlaylist } from '../types/media-playlist';
41
+ import type { NullableNetworkDetails } from '../types/network-details';
42
+
43
+ function mapContextToLevelType(
44
+ context: PlaylistLoaderContext,
45
+ ): PlaylistLevelType {
46
+ const { type } = context;
47
+
48
+ switch (type) {
49
+ case PlaylistContextType.AUDIO_TRACK:
50
+ return PlaylistLevelType.AUDIO;
51
+ case PlaylistContextType.SUBTITLE_TRACK:
52
+ return PlaylistLevelType.SUBTITLE;
53
+ default:
54
+ return PlaylistLevelType.MAIN;
55
+ }
56
+ }
57
+
58
+ function getResponseUrl(
59
+ response: LoaderResponse,
60
+ context: PlaylistLoaderContext,
61
+ ): string {
62
+ let url = response.url;
63
+ // responseURL not supported on some browsers (it is used to detect URL redirection)
64
+ // data-uri mode also not supported (but no need to detect redirection)
65
+ if (url === undefined || url.indexOf('data:') === 0) {
66
+ // fallback to initial URL
67
+ url = context.url;
68
+ }
69
+ return url;
70
+ }
71
+
72
+ class PlaylistLoader implements NetworkComponentAPI {
73
+ private readonly hls: Hls;
74
+ private readonly loaders: {
75
+ [key: string]: Loader<LoaderContext>;
76
+ } = Object.create(null);
77
+ private variableList: VariableMap | null = null;
78
+ public onManifestLoaded = this.checkAutostartLoad;
79
+
80
+ constructor(hls: Hls) {
81
+ this.hls = hls;
82
+ this.registerListeners();
83
+ }
84
+
85
+ public startLoad(startPosition: number): void {}
86
+
87
+ public stopLoad(): void {
88
+ this.destroyInternalLoaders();
89
+ }
90
+
91
+ private registerListeners() {
92
+ const { hls } = this;
93
+ hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
94
+ hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this);
95
+ hls.on(Events.AUDIO_TRACK_LOADING, this.onAudioTrackLoading, this);
96
+ hls.on(Events.SUBTITLE_TRACK_LOADING, this.onSubtitleTrackLoading, this);
97
+ hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this);
98
+ }
99
+
100
+ private unregisterListeners() {
101
+ const { hls } = this;
102
+ hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
103
+ hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this);
104
+ hls.off(Events.AUDIO_TRACK_LOADING, this.onAudioTrackLoading, this);
105
+ hls.off(Events.SUBTITLE_TRACK_LOADING, this.onSubtitleTrackLoading, this);
106
+ hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this);
107
+ }
108
+
109
+ /**
110
+ * Returns defaults or configured loader-type overloads (pLoader and loader config params)
111
+ */
112
+ private createInternalLoader(
113
+ context: PlaylistLoaderContext,
114
+ ): Loader<LoaderContext> {
115
+ const config = this.hls.config;
116
+ const PLoader = config.pLoader;
117
+ const Loader = config.loader;
118
+ const InternalLoader = PLoader || Loader;
119
+ const loader = new InternalLoader(config) as Loader<PlaylistLoaderContext>;
120
+
121
+ this.loaders[context.type] = loader;
122
+ return loader;
123
+ }
124
+
125
+ private getInternalLoader(
126
+ context: PlaylistLoaderContext,
127
+ ): Loader<LoaderContext> | undefined {
128
+ return this.loaders[context.type];
129
+ }
130
+
131
+ private resetInternalLoader(contextType: PlaylistContextType): void {
132
+ if (this.loaders[contextType]) {
133
+ delete this.loaders[contextType];
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Call `destroy` on all internal loader instances mapped (one per context type)
139
+ */
140
+ private destroyInternalLoaders(): void {
141
+ for (const contextType in this.loaders) {
142
+ const loader = this.loaders[contextType];
143
+ if (loader) {
144
+ loader.destroy();
145
+ }
146
+
147
+ this.resetInternalLoader(contextType as PlaylistContextType);
148
+ }
149
+ }
150
+
151
+ public destroy(): void {
152
+ this.variableList = null;
153
+ this.unregisterListeners();
154
+ this.destroyInternalLoaders();
155
+ }
156
+
157
+ private onManifestLoading(
158
+ event: Events.MANIFEST_LOADING,
159
+ data: ManifestLoadingData,
160
+ ) {
161
+ const { url } = data;
162
+ this.variableList = null;
163
+ this.load({
164
+ id: null,
165
+ level: 0,
166
+ responseType: 'text',
167
+ type: PlaylistContextType.MANIFEST,
168
+ url,
169
+ deliveryDirectives: null,
170
+ levelOrTrack: null,
171
+ });
172
+ }
173
+
174
+ private onLevelLoading(event: Events.LEVEL_LOADING, data: LevelLoadingData) {
175
+ const { id, level, pathwayId, url, deliveryDirectives, levelInfo } = data;
176
+ this.load({
177
+ id,
178
+ level,
179
+ pathwayId,
180
+ responseType: 'text',
181
+ type: PlaylistContextType.LEVEL,
182
+ url,
183
+ deliveryDirectives,
184
+ levelOrTrack: levelInfo,
185
+ });
186
+ }
187
+
188
+ private onAudioTrackLoading(
189
+ event: Events.AUDIO_TRACK_LOADING,
190
+ data: TrackLoadingData,
191
+ ) {
192
+ const { id, groupId, url, deliveryDirectives, track } = data;
193
+ this.load({
194
+ id,
195
+ groupId,
196
+ level: null,
197
+ responseType: 'text',
198
+ type: PlaylistContextType.AUDIO_TRACK,
199
+ url,
200
+ deliveryDirectives,
201
+ levelOrTrack: track,
202
+ });
203
+ }
204
+
205
+ private onSubtitleTrackLoading(
206
+ event: Events.SUBTITLE_TRACK_LOADING,
207
+ data: TrackLoadingData,
208
+ ) {
209
+ const { id, groupId, url, deliveryDirectives, track } = data;
210
+ this.load({
211
+ id,
212
+ groupId,
213
+ level: null,
214
+ responseType: 'text',
215
+ type: PlaylistContextType.SUBTITLE_TRACK,
216
+ url,
217
+ deliveryDirectives,
218
+ levelOrTrack: track,
219
+ });
220
+ }
221
+
222
+ private onLevelsUpdated(
223
+ event: Events.LEVELS_UPDATED,
224
+ data: LevelsUpdatedData,
225
+ ) {
226
+ // abort and delete loader of removed levels
227
+ const loader = this.loaders[PlaylistContextType.LEVEL];
228
+ if (loader) {
229
+ const context = loader.context;
230
+ if (
231
+ context &&
232
+ !data.levels.some(
233
+ (lvl) => lvl === (context as PlaylistLoaderContext).levelOrTrack,
234
+ )
235
+ ) {
236
+ loader.abort();
237
+ delete this.loaders[PlaylistContextType.LEVEL];
238
+ }
239
+ }
240
+ }
241
+
242
+ private load(context: PlaylistLoaderContext): void {
243
+ const config = this.hls.config;
244
+
245
+ // logger.debug(`[playlist-loader]: Loading playlist of type ${context.type}, level: ${context.level}, id: ${context.id}`);
246
+
247
+ // Check if a loader for this context already exists
248
+ let loader = this.getInternalLoader(context);
249
+ if (loader) {
250
+ const logger = this.hls.logger;
251
+ const loaderContext = loader.context as PlaylistLoaderContext;
252
+ if (
253
+ loaderContext &&
254
+ loaderContext.levelOrTrack === context.levelOrTrack &&
255
+ (loaderContext.url === context.url ||
256
+ (loaderContext.deliveryDirectives && !context.deliveryDirectives))
257
+ ) {
258
+ // same URL can't overlap, or wait for blocking request
259
+ if (loaderContext.url === context.url) {
260
+ logger.log(
261
+ `[playlist-loader]: ignore ${context.url} ongoing request`,
262
+ );
263
+ } else {
264
+ logger.log(
265
+ `[playlist-loader]: ignore ${context.url} in favor of ${loaderContext.url}`,
266
+ );
267
+ }
268
+ return;
269
+ }
270
+ logger.log(
271
+ `[playlist-loader]: aborting previous loader for type: ${context.type}`,
272
+ );
273
+ loader.abort();
274
+ }
275
+
276
+ // apply different configs for retries depending on
277
+ // context (manifest, level, audio/subs playlist)
278
+ let loadPolicy: LoaderConfig;
279
+ if (context.type === PlaylistContextType.MANIFEST) {
280
+ loadPolicy = config.manifestLoadPolicy.default;
281
+ } else {
282
+ loadPolicy = Object.assign({}, config.playlistLoadPolicy.default, {
283
+ timeoutRetry: null,
284
+ errorRetry: null,
285
+ });
286
+ }
287
+ loader = this.createInternalLoader(context);
288
+
289
+ // Override level/track timeout for LL-HLS requests
290
+ // (the default of 10000ms is counter productive to blocking playlist reload requests)
291
+ if (Number.isFinite(context.deliveryDirectives?.part)) {
292
+ let levelDetails: LevelDetails | undefined;
293
+ if (
294
+ context.type === PlaylistContextType.LEVEL &&
295
+ context.level !== null
296
+ ) {
297
+ levelDetails = this.hls.levels[context.level].details;
298
+ } else if (
299
+ context.type === PlaylistContextType.AUDIO_TRACK &&
300
+ context.id !== null
301
+ ) {
302
+ levelDetails = this.hls.audioTracks[context.id].details;
303
+ } else if (
304
+ context.type === PlaylistContextType.SUBTITLE_TRACK &&
305
+ context.id !== null
306
+ ) {
307
+ levelDetails = this.hls.subtitleTracks[context.id].details;
308
+ }
309
+ if (levelDetails) {
310
+ const partTarget = levelDetails.partTarget;
311
+ const targetDuration = levelDetails.targetduration;
312
+ if (partTarget && targetDuration) {
313
+ const maxLowLatencyPlaylistRefresh =
314
+ Math.max(partTarget * 3, targetDuration * 0.8) * 1000;
315
+ loadPolicy = Object.assign({}, loadPolicy, {
316
+ maxTimeToFirstByteMs: Math.min(
317
+ maxLowLatencyPlaylistRefresh,
318
+ loadPolicy.maxTimeToFirstByteMs,
319
+ ),
320
+ maxLoadTimeMs: Math.min(
321
+ maxLowLatencyPlaylistRefresh,
322
+ loadPolicy.maxTimeToFirstByteMs,
323
+ ),
324
+ });
325
+ }
326
+ }
327
+ }
328
+
329
+ const legacyRetryCompatibility: RetryConfig | Record<string, void> =
330
+ loadPolicy.errorRetry || loadPolicy.timeoutRetry || {};
331
+ const loaderConfig: LoaderConfiguration = {
332
+ loadPolicy,
333
+ timeout: loadPolicy.maxLoadTimeMs,
334
+ maxRetry: legacyRetryCompatibility.maxNumRetry || 0,
335
+ retryDelay: legacyRetryCompatibility.retryDelayMs || 0,
336
+ maxRetryDelay: legacyRetryCompatibility.maxRetryDelayMs || 0,
337
+ };
338
+
339
+ const loaderCallbacks: LoaderCallbacks<PlaylistLoaderContext> = {
340
+ onSuccess: (response, stats, context, networkDetails) => {
341
+ const loader = this.getInternalLoader(context) as
342
+ | Loader<PlaylistLoaderContext>
343
+ | undefined;
344
+ this.resetInternalLoader(context.type);
345
+
346
+ const string = response.data as string;
347
+
348
+ stats.parsing.start = performance.now();
349
+ if (
350
+ M3U8Parser.isMediaPlaylist(string) ||
351
+ context.type !== PlaylistContextType.MANIFEST
352
+ ) {
353
+ this.handleTrackOrLevelPlaylist(
354
+ response,
355
+ stats,
356
+ context,
357
+ networkDetails || null,
358
+ loader,
359
+ );
360
+ } else {
361
+ this.handleMasterPlaylist(response, stats, context, networkDetails);
362
+ }
363
+ },
364
+ onError: (response, context, networkDetails, stats) => {
365
+ this.handleNetworkError(
366
+ context,
367
+ networkDetails,
368
+ false,
369
+ response,
370
+ stats,
371
+ );
372
+ },
373
+ onTimeout: (stats, context, networkDetails) => {
374
+ this.handleNetworkError(
375
+ context,
376
+ networkDetails,
377
+ true,
378
+ undefined,
379
+ stats,
380
+ );
381
+ },
382
+ };
383
+
384
+ // logger.debug(`[playlist-loader]: Calling internal loader delegate for URL: ${context.url}`);
385
+
386
+ loader.load(context, loaderConfig, loaderCallbacks);
387
+ }
388
+
389
+ private checkAutostartLoad() {
390
+ if (!this.hls) {
391
+ return;
392
+ }
393
+ const {
394
+ config: { autoStartLoad, startPosition },
395
+ forceStartLoad,
396
+ } = this.hls;
397
+ if (autoStartLoad || forceStartLoad) {
398
+ this.hls.logger.log(
399
+ `${autoStartLoad ? 'auto' : 'force'} startLoad with configured startPosition ${startPosition}`,
400
+ );
401
+ this.hls.startLoad(startPosition);
402
+ }
403
+ }
404
+
405
+ private handleMasterPlaylist(
406
+ response: LoaderResponse,
407
+ stats: LoaderStats,
408
+ context: PlaylistLoaderContext,
409
+ networkDetails: NullableNetworkDetails,
410
+ ): void {
411
+ const hls = this.hls;
412
+ const string = response.data as string;
413
+
414
+ const url = getResponseUrl(response, context);
415
+
416
+ const parsedResult = M3U8Parser.parseMasterPlaylist(string, url);
417
+
418
+ if (parsedResult.playlistParsingError) {
419
+ stats.parsing.end = performance.now();
420
+ this.handleManifestParsingError(
421
+ response,
422
+ context,
423
+ parsedResult.playlistParsingError,
424
+ networkDetails,
425
+ stats,
426
+ );
427
+ return;
428
+ }
429
+
430
+ const {
431
+ contentSteering,
432
+ levels,
433
+ sessionData,
434
+ sessionKeys,
435
+ startTimeOffset,
436
+ variableList,
437
+ } = parsedResult;
438
+
439
+ this.variableList = variableList;
440
+
441
+ // Treat unknown codec as audio or video codec based on passing `isTypeSupported` check
442
+ // (allows for playback of any supported codec even if not indexed in utils/codecs)
443
+ levels.forEach((levelParsed: LevelParsed) => {
444
+ const { unknownCodecs } = levelParsed;
445
+ if (unknownCodecs) {
446
+ const { preferManagedMediaSource } = this.hls.config;
447
+ let { audioCodec, videoCodec } = levelParsed;
448
+ for (let i = unknownCodecs.length; i--; ) {
449
+ const unknownCodec = unknownCodecs[i];
450
+ if (
451
+ areCodecsMediaSourceSupported(
452
+ unknownCodec,
453
+ 'audio',
454
+ preferManagedMediaSource,
455
+ )
456
+ ) {
457
+ levelParsed.audioCodec = audioCodec = audioCodec
458
+ ? `${audioCodec},${unknownCodec}`
459
+ : unknownCodec;
460
+ sampleEntryCodesISO.audio[audioCodec.substring(0, 4)] = 2;
461
+ unknownCodecs.splice(i, 1);
462
+ } else if (
463
+ areCodecsMediaSourceSupported(
464
+ unknownCodec,
465
+ 'video',
466
+ preferManagedMediaSource,
467
+ )
468
+ ) {
469
+ levelParsed.videoCodec = videoCodec = videoCodec
470
+ ? `${videoCodec},${unknownCodec}`
471
+ : unknownCodec;
472
+ sampleEntryCodesISO.video[videoCodec.substring(0, 4)] = 2;
473
+ unknownCodecs.splice(i, 1);
474
+ }
475
+ }
476
+ }
477
+ });
478
+
479
+ const {
480
+ AUDIO: audioTracks = [],
481
+ SUBTITLES: subtitles,
482
+ 'CLOSED-CAPTIONS': captions,
483
+ } = M3U8Parser.parseMasterPlaylistMedia(string, url, parsedResult);
484
+
485
+ if (audioTracks.length) {
486
+ // check if we have found an audio track embedded in main playlist (audio track without URI attribute)
487
+ const embeddedAudioFound: boolean = audioTracks.some(
488
+ (audioTrack) => !audioTrack.url,
489
+ );
490
+
491
+ // if no embedded audio track defined, but audio codec signaled in quality level,
492
+ // we need to signal this main audio track this could happen with playlists with
493
+ // alt audio rendition in which quality levels (main)
494
+ // contains both audio+video. but with mixed audio track not signaled
495
+ if (
496
+ !embeddedAudioFound &&
497
+ levels[0].audioCodec &&
498
+ !levels[0].attrs.AUDIO
499
+ ) {
500
+ this.hls.logger.log(
501
+ '[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one',
502
+ );
503
+ audioTracks.unshift({
504
+ type: 'main',
505
+ name: 'main',
506
+ groupId: 'main',
507
+ default: false,
508
+ autoselect: false,
509
+ forced: false,
510
+ id: -1,
511
+ attrs: new AttrList({}) as MediaAttributes,
512
+ bitrate: 0,
513
+ url: '',
514
+ });
515
+ }
516
+ }
517
+
518
+ hls.trigger(Events.MANIFEST_LOADED, {
519
+ levels,
520
+ audioTracks,
521
+ subtitles,
522
+ captions,
523
+ contentSteering,
524
+ url,
525
+ stats,
526
+ networkDetails,
527
+ sessionData,
528
+ sessionKeys,
529
+ startTimeOffset,
530
+ variableList,
531
+ });
532
+ }
533
+
534
+ private handleTrackOrLevelPlaylist(
535
+ response: LoaderResponse,
536
+ stats: LoaderStats,
537
+ context: PlaylistLoaderContext,
538
+ networkDetails: NullableNetworkDetails,
539
+ loader: Loader<PlaylistLoaderContext> | undefined,
540
+ ): void {
541
+ const hls = this.hls;
542
+ const { id, level, type } = context;
543
+
544
+ const url = getResponseUrl(response, context);
545
+ const levelId = Number.isFinite(level as number)
546
+ ? (level as number)
547
+ : Number.isFinite(id as number)
548
+ ? (id as number)
549
+ : 0;
550
+ const levelType = mapContextToLevelType(context);
551
+ // 仅在启用算法数据加载时跳过算法分片
552
+ const algoSegmentPattern = this.hls.config.algoDataEnabled
553
+ ? this.hls.config.algoSegmentPattern
554
+ : null;
555
+ const levelDetails = M3U8Parser.parseLevelPlaylist(
556
+ response.data as string,
557
+ url,
558
+ levelId,
559
+ levelType,
560
+ 0,
561
+ this.variableList,
562
+ algoSegmentPattern,
563
+ );
564
+
565
+ // We have done our first request (Manifest-type) and receive
566
+ // not a master playlist but a chunk-list (track/level)
567
+ // We fire the manifest-loaded event anyway with the parsed level-details
568
+ // by creating a single-level structure for it.
569
+ if (type === PlaylistContextType.MANIFEST) {
570
+ const singleLevel: LevelParsed = {
571
+ attrs: new AttrList({}),
572
+ bitrate: 0,
573
+ details: levelDetails,
574
+ name: '',
575
+ url,
576
+ };
577
+ levelDetails.requestScheduled =
578
+ stats.loading.start + computeReloadInterval(levelDetails, 0);
579
+
580
+ hls.trigger(Events.MANIFEST_LOADED, {
581
+ levels: [singleLevel],
582
+ audioTracks: [],
583
+ url,
584
+ stats,
585
+ networkDetails,
586
+ sessionData: null,
587
+ sessionKeys: null,
588
+ contentSteering: null,
589
+ startTimeOffset: null,
590
+ variableList: null,
591
+ });
592
+ }
593
+
594
+ // save parsing time
595
+ stats.parsing.end = performance.now();
596
+
597
+ // extend the context with the new levelDetails property
598
+ context.levelDetails = levelDetails;
599
+
600
+ this.handlePlaylistLoaded(
601
+ levelDetails,
602
+ response,
603
+ stats,
604
+ context,
605
+ networkDetails,
606
+ loader,
607
+ );
608
+ }
609
+
610
+ private handleManifestParsingError(
611
+ response: LoaderResponse,
612
+ context: PlaylistLoaderContext,
613
+ error: Error,
614
+ networkDetails: NullableNetworkDetails,
615
+ stats: LoaderStats,
616
+ ): void {
617
+ this.hls.trigger(Events.ERROR, {
618
+ type: ErrorTypes.NETWORK_ERROR,
619
+ details: ErrorDetails.MANIFEST_PARSING_ERROR,
620
+ fatal: context.type === PlaylistContextType.MANIFEST,
621
+ url: response.url,
622
+ err: error,
623
+ error,
624
+ reason: error.message,
625
+ response,
626
+ context,
627
+ networkDetails,
628
+ stats,
629
+ });
630
+ }
631
+
632
+ private handleNetworkError(
633
+ context: PlaylistLoaderContext,
634
+ networkDetails: NullableNetworkDetails,
635
+ timeout = false,
636
+ response: { code: number; text: string } | undefined,
637
+ stats: LoaderStats,
638
+ ): void {
639
+ let message = `A network ${
640
+ timeout
641
+ ? 'timeout'
642
+ : 'error' + (response ? ' (status ' + response.code + ')' : '')
643
+ } occurred while loading ${context.type}`;
644
+ if (context.type === PlaylistContextType.LEVEL) {
645
+ message += `: ${context.level} id: ${context.id}`;
646
+ } else if (
647
+ context.type === PlaylistContextType.AUDIO_TRACK ||
648
+ context.type === PlaylistContextType.SUBTITLE_TRACK
649
+ ) {
650
+ message += ` id: ${context.id} group-id: "${context.groupId}"`;
651
+ }
652
+ const error = new Error(message);
653
+ this.hls.logger.warn(`[playlist-loader]: ${message}`);
654
+ let details = ErrorDetails.UNKNOWN;
655
+ let fatal = false;
656
+
657
+ const loader = this.getInternalLoader(context);
658
+
659
+ switch (context.type) {
660
+ case PlaylistContextType.MANIFEST:
661
+ details = timeout
662
+ ? ErrorDetails.MANIFEST_LOAD_TIMEOUT
663
+ : ErrorDetails.MANIFEST_LOAD_ERROR;
664
+ fatal = true;
665
+ break;
666
+ case PlaylistContextType.LEVEL:
667
+ details = timeout
668
+ ? ErrorDetails.LEVEL_LOAD_TIMEOUT
669
+ : ErrorDetails.LEVEL_LOAD_ERROR;
670
+ fatal = false;
671
+ break;
672
+ case PlaylistContextType.AUDIO_TRACK:
673
+ details = timeout
674
+ ? ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT
675
+ : ErrorDetails.AUDIO_TRACK_LOAD_ERROR;
676
+ fatal = false;
677
+ break;
678
+ case PlaylistContextType.SUBTITLE_TRACK:
679
+ details = timeout
680
+ ? ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT
681
+ : ErrorDetails.SUBTITLE_LOAD_ERROR;
682
+ fatal = false;
683
+ break;
684
+ }
685
+
686
+ if (loader) {
687
+ this.resetInternalLoader(context.type);
688
+ }
689
+
690
+ const errorData: ErrorData = {
691
+ type: ErrorTypes.NETWORK_ERROR,
692
+ details,
693
+ fatal,
694
+ url: context.url,
695
+ loader,
696
+ context,
697
+ error,
698
+ networkDetails,
699
+ stats,
700
+ };
701
+
702
+ if (response) {
703
+ let url = context.url;
704
+ if (networkDetails && 'url' in networkDetails) {
705
+ url = networkDetails.url;
706
+ }
707
+ errorData.response = { url, data: undefined as any, ...response };
708
+ }
709
+
710
+ this.hls.trigger(Events.ERROR, errorData);
711
+ }
712
+
713
+ private handlePlaylistLoaded(
714
+ levelDetails: LevelDetails,
715
+ response: LoaderResponse,
716
+ stats: LoaderStats,
717
+ context: PlaylistLoaderContext,
718
+ networkDetails: NullableNetworkDetails,
719
+ loader: Loader<PlaylistLoaderContext> | undefined,
720
+ ): void {
721
+ const hls = this.hls;
722
+ const { type, level, levelOrTrack, id, groupId, deliveryDirectives } =
723
+ context;
724
+ const url = getResponseUrl(response, context);
725
+ const parent = mapContextToLevelType(context);
726
+ let levelIndex =
727
+ typeof context.level === 'number' && parent === PlaylistLevelType.MAIN
728
+ ? (level as number)
729
+ : undefined;
730
+ const error = levelDetails.playlistParsingError;
731
+ if (error) {
732
+ this.hls.logger.warn(`${error} ${levelDetails.url}`);
733
+ if (!hls.config.ignorePlaylistParsingErrors) {
734
+ hls.trigger(Events.ERROR, {
735
+ type: ErrorTypes.NETWORK_ERROR,
736
+ details: ErrorDetails.LEVEL_PARSING_ERROR,
737
+ fatal: false,
738
+ url,
739
+ error,
740
+ reason: error.message,
741
+ response,
742
+ context,
743
+ level: levelIndex,
744
+ parent,
745
+ networkDetails,
746
+ stats,
747
+ });
748
+ return;
749
+ }
750
+ levelDetails.playlistParsingError = null;
751
+ }
752
+ if (!levelDetails.fragments.length) {
753
+ const error = (levelDetails.playlistParsingError = new Error(
754
+ 'No Segments found in Playlist',
755
+ ));
756
+ hls.trigger(Events.ERROR, {
757
+ type: ErrorTypes.NETWORK_ERROR,
758
+ details: ErrorDetails.LEVEL_EMPTY_ERROR,
759
+ fatal: false,
760
+ url,
761
+ error,
762
+ reason: error.message,
763
+ response,
764
+ context,
765
+ level: levelIndex,
766
+ parent,
767
+ networkDetails,
768
+ stats,
769
+ });
770
+ return;
771
+ }
772
+
773
+ if (levelDetails.live && loader) {
774
+ if (loader.getCacheAge) {
775
+ levelDetails.ageHeader = loader.getCacheAge() || 0;
776
+ }
777
+ if (!loader.getCacheAge || isNaN(levelDetails.ageHeader)) {
778
+ levelDetails.ageHeader = 0;
779
+ }
780
+ }
781
+
782
+ switch (type) {
783
+ case PlaylistContextType.MANIFEST:
784
+ case PlaylistContextType.LEVEL:
785
+ if (levelIndex) {
786
+ if (!levelOrTrack) {
787
+ // fall-through to hls.levels[0]
788
+ levelIndex = 0;
789
+ } else {
790
+ if (levelOrTrack !== hls.levels[levelIndex]) {
791
+ // correct levelIndex when lower levels were removed from hls.levels
792
+ const updatedIndex = hls.levels.indexOf(levelOrTrack as Level);
793
+ if (updatedIndex > -1) {
794
+ levelIndex = updatedIndex;
795
+ }
796
+ }
797
+ }
798
+ }
799
+ hls.trigger(Events.LEVEL_LOADED, {
800
+ details: levelDetails,
801
+ levelInfo: (levelOrTrack as Level | null) || hls.levels[0],
802
+ level: levelIndex || 0,
803
+ id: id || 0,
804
+ stats,
805
+ networkDetails,
806
+ deliveryDirectives,
807
+ withoutMultiVariant: type === PlaylistContextType.MANIFEST,
808
+ context,
809
+ });
810
+ break;
811
+ case PlaylistContextType.AUDIO_TRACK:
812
+ hls.trigger(Events.AUDIO_TRACK_LOADED, {
813
+ details: levelDetails,
814
+ track: levelOrTrack as MediaPlaylist,
815
+ id: id || 0,
816
+ groupId: groupId || '',
817
+ stats,
818
+ networkDetails,
819
+ deliveryDirectives,
820
+ context,
821
+ });
822
+ break;
823
+ case PlaylistContextType.SUBTITLE_TRACK:
824
+ hls.trigger(Events.SUBTITLE_TRACK_LOADED, {
825
+ details: levelDetails,
826
+ track: levelOrTrack as MediaPlaylist,
827
+ id: id || 0,
828
+ groupId: groupId || '',
829
+ stats,
830
+ networkDetails,
831
+ deliveryDirectives,
832
+ context,
833
+ });
834
+ break;
835
+ }
836
+ }
837
+ }
838
+
839
+ export default PlaylistLoader;