@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,776 @@
1
+ import BasePlaylistController from './base-playlist-controller';
2
+ import { ErrorDetails, ErrorTypes } from '../errors';
3
+ import { Events } from '../events';
4
+ import { isVideoRange, Level, VideoRangeValues } from '../types/level';
5
+ import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
6
+ import {
7
+ areCodecsMediaSourceSupported,
8
+ codecsSetSelectionPreferenceValue,
9
+ convertAVC1ToAVCOTI,
10
+ getCodecCompatibleName,
11
+ videoCodecPreferenceValue,
12
+ } from '../utils/codecs';
13
+ import { reassignFragmentLevelIndexes } from '../utils/level-helper';
14
+ import { getUnsupportedResult } from '../utils/mediacapabilities-helper';
15
+ import { stringify } from '../utils/safe-json-stringify';
16
+ import type ContentSteeringController from './content-steering-controller';
17
+ import type Hls from '../hls';
18
+ import type {
19
+ ErrorData,
20
+ FragBufferedData,
21
+ LevelLoadedData,
22
+ LevelsUpdatedData,
23
+ LevelSwitchingData,
24
+ ManifestLoadedData,
25
+ ManifestLoadingData,
26
+ ManifestParsedData,
27
+ } from '../types/events';
28
+ import type { HlsUrlParameters, LevelParsed } from '../types/level';
29
+ import type { MediaPlaylist } from '../types/media-playlist';
30
+
31
+ export default class LevelController extends BasePlaylistController {
32
+ private _levels: Level[] = [];
33
+ private _firstLevel: number = -1;
34
+ private _maxAutoLevel: number = -1;
35
+ private _startLevel?: number;
36
+ private currentLevel: Level | null = null;
37
+ private currentLevelIndex: number = -1;
38
+ private manualLevelIndex: number = -1;
39
+ private steering: ContentSteeringController | null;
40
+ private lastABRSwitchTime: number = -1;
41
+
42
+ public onParsedComplete!: Function;
43
+
44
+ constructor(
45
+ hls: Hls,
46
+ contentSteeringController: ContentSteeringController | null,
47
+ ) {
48
+ super(hls, 'level-controller');
49
+ this.steering = contentSteeringController;
50
+ this._registerListeners();
51
+ }
52
+
53
+ private _registerListeners() {
54
+ const { hls } = this;
55
+ hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
56
+ hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
57
+ hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
58
+ hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this);
59
+ hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
60
+ hls.on(Events.ERROR, this.onError, this);
61
+ }
62
+
63
+ private _unregisterListeners() {
64
+ const { hls } = this;
65
+ hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
66
+ hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
67
+ hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
68
+ hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this);
69
+ hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
70
+ hls.off(Events.ERROR, this.onError, this);
71
+ }
72
+
73
+ public destroy() {
74
+ this._unregisterListeners();
75
+ this.steering = null;
76
+ this.resetLevels();
77
+ super.destroy();
78
+ }
79
+
80
+ public stopLoad(): void {
81
+ const levels = this._levels;
82
+
83
+ // clean up live level details to force reload them, and reset load errors
84
+ levels.forEach((level) => {
85
+ level.loadError = 0;
86
+ level.fragmentError = 0;
87
+ });
88
+
89
+ super.stopLoad();
90
+ }
91
+
92
+ private resetLevels() {
93
+ this._startLevel = undefined;
94
+ this.manualLevelIndex = -1;
95
+ this.currentLevelIndex = -1;
96
+ this.currentLevel = null;
97
+ this._levels = [];
98
+ this._maxAutoLevel = -1;
99
+ }
100
+
101
+ private onManifestLoading(
102
+ event: Events.MANIFEST_LOADING,
103
+ data: ManifestLoadingData,
104
+ ) {
105
+ this.resetLevels();
106
+ }
107
+
108
+ protected onManifestLoaded(
109
+ event: Events.MANIFEST_LOADED,
110
+ data: ManifestLoadedData,
111
+ ) {
112
+ const preferManagedMediaSource = this.hls.config.preferManagedMediaSource;
113
+ const levels: Level[] = [];
114
+ const redundantSet: { [key: string]: Level } = {};
115
+ const generatePathwaySet: { [key: string]: number } = {};
116
+ let resolutionFound = false;
117
+ let videoCodecFound = false;
118
+ let audioCodecFound = false;
119
+
120
+ data.levels.forEach((levelParsed: LevelParsed) => {
121
+ const attributes = levelParsed.attrs;
122
+ let { audioCodec, videoCodec } = levelParsed;
123
+ if (audioCodec) {
124
+ // Returns empty and set to undefined for 'mp4a.40.34' with fallback to 'audio/mpeg' SourceBuffer
125
+ levelParsed.audioCodec = audioCodec =
126
+ getCodecCompatibleName(audioCodec, preferManagedMediaSource) ||
127
+ undefined;
128
+ }
129
+
130
+ if (videoCodec) {
131
+ videoCodec = levelParsed.videoCodec = convertAVC1ToAVCOTI(videoCodec);
132
+ }
133
+
134
+ // only keep levels with supported audio/video codecs
135
+ const { width, height, unknownCodecs } = levelParsed;
136
+ const unknownUnsupportedCodecCount = unknownCodecs?.length || 0;
137
+
138
+ resolutionFound ||= !!(width && height);
139
+ videoCodecFound ||= !!videoCodec;
140
+ audioCodecFound ||= !!audioCodec;
141
+ if (
142
+ unknownUnsupportedCodecCount ||
143
+ (audioCodec && !this.isAudioSupported(audioCodec)) ||
144
+ (videoCodec && !this.isVideoSupported(videoCodec))
145
+ ) {
146
+ this.log(`Some or all CODECS not supported "${attributes.CODECS}"`);
147
+ return;
148
+ }
149
+
150
+ const {
151
+ CODECS,
152
+ 'FRAME-RATE': FRAMERATE,
153
+ 'HDCP-LEVEL': HDCP,
154
+ 'PATHWAY-ID': PATHWAY,
155
+ RESOLUTION,
156
+ 'VIDEO-RANGE': VIDEO_RANGE,
157
+ } = attributes;
158
+ const contentSteeringPrefix = `${PATHWAY || '.'}-`;
159
+ const levelKey = `${contentSteeringPrefix}${levelParsed.bitrate}-${RESOLUTION}-${FRAMERATE}-${CODECS}-${VIDEO_RANGE}-${HDCP}`;
160
+
161
+ if (!redundantSet[levelKey]) {
162
+ const level = this.createLevel(levelParsed);
163
+ redundantSet[levelKey] = level;
164
+ generatePathwaySet[levelKey] = 1;
165
+ levels.push(level);
166
+ } else if (
167
+ redundantSet[levelKey].uri !== levelParsed.url &&
168
+ !levelParsed.attrs['PATHWAY-ID']
169
+ ) {
170
+ // Assign Pathway IDs to Redundant Streams (default Pathways is ".". Redundant Streams "..", "...", and so on.)
171
+ // Content Steering controller to handles Pathway fallback on error
172
+ const pathwayCount = (generatePathwaySet[levelKey] += 1);
173
+ levelParsed.attrs['PATHWAY-ID'] = new Array(pathwayCount + 1).join('.');
174
+ const level = this.createLevel(levelParsed);
175
+ redundantSet[levelKey] = level;
176
+ levels.push(level);
177
+ } else {
178
+ redundantSet[levelKey].addGroupId('audio', attributes.AUDIO);
179
+ redundantSet[levelKey].addGroupId('text', attributes.SUBTITLES);
180
+ }
181
+ });
182
+
183
+ this.filterAndSortMediaOptions(
184
+ levels,
185
+ data,
186
+ resolutionFound,
187
+ videoCodecFound,
188
+ audioCodecFound,
189
+ );
190
+ }
191
+
192
+ private createLevel(levelParsed: LevelParsed): Level {
193
+ const level = new Level(levelParsed);
194
+ const supplemental = levelParsed.supplemental;
195
+ if (
196
+ supplemental?.videoCodec &&
197
+ !this.isVideoSupported(supplemental.videoCodec)
198
+ ) {
199
+ const error = new Error(
200
+ `SUPPLEMENTAL-CODECS not supported "${supplemental.videoCodec}"`,
201
+ );
202
+ this.log(error.message);
203
+ level.supportedResult = getUnsupportedResult(error, []);
204
+ }
205
+ return level;
206
+ }
207
+
208
+ private isAudioSupported(codec: string): boolean {
209
+ return areCodecsMediaSourceSupported(
210
+ codec,
211
+ 'audio',
212
+ this.hls.config.preferManagedMediaSource,
213
+ );
214
+ }
215
+
216
+ private isVideoSupported(codec: string): boolean {
217
+ return areCodecsMediaSourceSupported(
218
+ codec,
219
+ 'video',
220
+ this.hls.config.preferManagedMediaSource,
221
+ );
222
+ }
223
+
224
+ private filterAndSortMediaOptions(
225
+ filteredLevels: Level[],
226
+ data: ManifestLoadedData,
227
+ resolutionFound: boolean,
228
+ videoCodecFound: boolean,
229
+ audioCodecFound: boolean,
230
+ ) {
231
+ let audioTracks: MediaPlaylist[] = [];
232
+ let subtitleTracks: MediaPlaylist[] = [];
233
+ let levels = filteredLevels;
234
+ const statsParsing = data.stats?.parsing || {};
235
+
236
+ // remove audio-only and invalid video-range levels if we also have levels with video codecs or RESOLUTION signalled
237
+ if ((resolutionFound || videoCodecFound) && audioCodecFound) {
238
+ levels = levels.filter(
239
+ ({ videoCodec, videoRange, width, height }) =>
240
+ (!!videoCodec || !!(width && height)) && isVideoRange(videoRange),
241
+ );
242
+ }
243
+
244
+ if (levels.length === 0) {
245
+ // Dispatch error after MANIFEST_LOADED is done propagating
246
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
247
+ Promise.resolve().then(() => {
248
+ if (this.hls) {
249
+ let message = 'no level with compatible codecs found in manifest';
250
+ let reason = message;
251
+ if (data.levels.length) {
252
+ reason = `one or more CODECS in variant not supported: ${stringify(
253
+ data.levels
254
+ .map((level) => level.attrs.CODECS)
255
+ .filter(
256
+ (value, index, array) => array.indexOf(value) === index,
257
+ ),
258
+ )}`;
259
+ this.warn(reason);
260
+ message += ` (${reason})`;
261
+ }
262
+ const error = new Error(message);
263
+ this.hls.trigger(Events.ERROR, {
264
+ type: ErrorTypes.MEDIA_ERROR,
265
+ details: ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR,
266
+ fatal: true,
267
+ url: data.url,
268
+ error,
269
+ reason,
270
+ });
271
+ }
272
+ });
273
+ statsParsing.end = performance.now();
274
+ return;
275
+ }
276
+
277
+ if (data.audioTracks) {
278
+ audioTracks = data.audioTracks.filter(
279
+ (track) => !track.audioCodec || this.isAudioSupported(track.audioCodec),
280
+ );
281
+ // Assign ids after filtering as array indices by group-id
282
+ assignTrackIdsByGroup(audioTracks);
283
+ }
284
+
285
+ if (data.subtitles) {
286
+ subtitleTracks = data.subtitles;
287
+ assignTrackIdsByGroup(subtitleTracks);
288
+ }
289
+ // start bitrate is the first bitrate of the manifest
290
+ const unsortedLevels = levels.slice(0);
291
+ // sort levels from lowest to highest
292
+ levels.sort((a, b) => {
293
+ if (a.attrs['HDCP-LEVEL'] !== b.attrs['HDCP-LEVEL']) {
294
+ return (a.attrs['HDCP-LEVEL'] || '') > (b.attrs['HDCP-LEVEL'] || '')
295
+ ? 1
296
+ : -1;
297
+ }
298
+ // sort on height before bitrate for cap-level-controller
299
+ if (resolutionFound && a.height !== b.height) {
300
+ return a.height - b.height;
301
+ }
302
+ if (a.frameRate !== b.frameRate) {
303
+ return a.frameRate - b.frameRate;
304
+ }
305
+ if (a.videoRange !== b.videoRange) {
306
+ return (
307
+ VideoRangeValues.indexOf(a.videoRange) -
308
+ VideoRangeValues.indexOf(b.videoRange)
309
+ );
310
+ }
311
+ if (a.videoCodec !== b.videoCodec) {
312
+ const valueA = videoCodecPreferenceValue(a.videoCodec);
313
+ const valueB = videoCodecPreferenceValue(b.videoCodec);
314
+ if (valueA !== valueB) {
315
+ return valueB - valueA;
316
+ }
317
+ }
318
+ if (a.uri === b.uri && a.codecSet !== b.codecSet) {
319
+ const valueA = codecsSetSelectionPreferenceValue(a.codecSet);
320
+ const valueB = codecsSetSelectionPreferenceValue(b.codecSet);
321
+ if (valueA !== valueB) {
322
+ return valueB - valueA;
323
+ }
324
+ }
325
+ if (a.averageBitrate !== b.averageBitrate) {
326
+ return a.averageBitrate - b.averageBitrate;
327
+ }
328
+ return 0;
329
+ });
330
+
331
+ let firstLevelInPlaylist = unsortedLevels[0];
332
+ if (this.steering) {
333
+ levels = this.steering.filterParsedLevels(levels);
334
+ if (levels.length !== unsortedLevels.length) {
335
+ for (let i = 0; i < unsortedLevels.length; i++) {
336
+ if (unsortedLevels[i].pathwayId === levels[0].pathwayId) {
337
+ firstLevelInPlaylist = unsortedLevels[i];
338
+ break;
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+ this._levels = levels;
345
+
346
+ // find index of first level in sorted levels
347
+ for (let i = 0; i < levels.length; i++) {
348
+ if (levels[i] === firstLevelInPlaylist) {
349
+ this._firstLevel = i;
350
+ const firstLevelBitrate = firstLevelInPlaylist.bitrate;
351
+ const bandwidthEstimate = this.hls.bandwidthEstimate;
352
+ this.log(
353
+ `manifest loaded, ${levels.length} level(s) found, first bitrate: ${firstLevelBitrate}`,
354
+ );
355
+ // Update default bwe to first variant bitrate as long it has not been configured or set
356
+ if (this.hls.userConfig?.abrEwmaDefaultEstimate === undefined) {
357
+ const startingBwEstimate = Math.min(
358
+ firstLevelBitrate,
359
+ this.hls.config.abrEwmaDefaultEstimateMax,
360
+ );
361
+ if (
362
+ startingBwEstimate > bandwidthEstimate &&
363
+ bandwidthEstimate === this.hls.abrEwmaDefaultEstimate
364
+ ) {
365
+ this.hls.bandwidthEstimate = startingBwEstimate;
366
+ }
367
+ }
368
+ break;
369
+ }
370
+ }
371
+
372
+ // Audio is only alternate if manifest include a URI along with the audio group tag,
373
+ // and this is not an audio-only stream where levels contain audio-only
374
+ const audioOnly = audioCodecFound && !videoCodecFound;
375
+ const config = this.hls.config;
376
+ const altAudioEnabled = !!(
377
+ config.audioStreamController && config.audioTrackController
378
+ );
379
+ const edata: ManifestParsedData = {
380
+ levels,
381
+ audioTracks,
382
+ subtitleTracks,
383
+ sessionData: data.sessionData,
384
+ sessionKeys: data.sessionKeys,
385
+ firstLevel: this._firstLevel,
386
+ stats: data.stats,
387
+ audio: audioCodecFound,
388
+ video: videoCodecFound,
389
+ altAudio:
390
+ altAudioEnabled && !audioOnly && audioTracks.some((t) => !!t.url),
391
+ };
392
+ statsParsing.end = performance.now();
393
+ this.hls.trigger(Events.MANIFEST_PARSED, edata);
394
+ }
395
+
396
+ get levels(): Level[] | null {
397
+ if (this._levels.length === 0) {
398
+ return null;
399
+ }
400
+ return this._levels;
401
+ }
402
+
403
+ get loadLevelObj(): Level | null {
404
+ return this.currentLevel;
405
+ }
406
+
407
+ get level(): number {
408
+ return this.currentLevelIndex;
409
+ }
410
+
411
+ set level(newLevel: number) {
412
+ const levels = this._levels;
413
+ if (levels.length === 0) {
414
+ return;
415
+ }
416
+ // check if level idx is valid
417
+ if (newLevel < 0 || newLevel >= levels.length) {
418
+ // invalid level id given, trigger error
419
+ const error = new Error('invalid level idx');
420
+ const fatal = newLevel < 0;
421
+ this.hls.trigger(Events.ERROR, {
422
+ type: ErrorTypes.OTHER_ERROR,
423
+ details: ErrorDetails.LEVEL_SWITCH_ERROR,
424
+ level: newLevel,
425
+ fatal,
426
+ error,
427
+ reason: error.message,
428
+ });
429
+ if (fatal) {
430
+ return;
431
+ }
432
+ newLevel = Math.min(newLevel, levels.length - 1);
433
+ }
434
+
435
+ const lastLevelIndex = this.currentLevelIndex;
436
+ const lastLevel = this.currentLevel;
437
+ const lastPathwayId = lastLevel ? lastLevel.attrs['PATHWAY-ID'] : undefined;
438
+ const level = levels[newLevel];
439
+ const pathwayId = level.attrs['PATHWAY-ID'];
440
+ this.currentLevelIndex = newLevel;
441
+ this.currentLevel = level;
442
+
443
+ if (
444
+ lastLevelIndex === newLevel &&
445
+ lastLevel &&
446
+ lastPathwayId === pathwayId
447
+ ) {
448
+ return;
449
+ }
450
+
451
+ this.log(
452
+ `Switching to level ${newLevel} (${
453
+ level.height ? level.height + 'p ' : ''
454
+ }${level.videoRange ? level.videoRange + ' ' : ''}${
455
+ level.codecSet ? level.codecSet + ' ' : ''
456
+ }@${level.bitrate})${
457
+ pathwayId ? ' with Pathway ' + pathwayId : ''
458
+ } from level ${lastLevelIndex}${
459
+ lastPathwayId ? ' with Pathway ' + lastPathwayId : ''
460
+ }`,
461
+ );
462
+
463
+ const levelSwitchingData: LevelSwitchingData = {
464
+ level: newLevel,
465
+ attrs: level.attrs,
466
+ details: level.details,
467
+ bitrate: level.bitrate,
468
+ averageBitrate: level.averageBitrate,
469
+ maxBitrate: level.maxBitrate,
470
+ realBitrate: level.realBitrate,
471
+ width: level.width,
472
+ height: level.height,
473
+ codecSet: level.codecSet,
474
+ audioCodec: level.audioCodec,
475
+ videoCodec: level.videoCodec,
476
+ audioGroups: level.audioGroups,
477
+ subtitleGroups: level.subtitleGroups,
478
+ loaded: level.loaded,
479
+ loadError: level.loadError,
480
+ fragmentError: level.fragmentError,
481
+ name: level.name,
482
+ id: level.id,
483
+ uri: level.uri,
484
+ url: level.url,
485
+ urlId: 0,
486
+ audioGroupIds: level.audioGroupIds,
487
+ textGroupIds: level.textGroupIds,
488
+ };
489
+
490
+ this.hls.trigger(Events.LEVEL_SWITCHING, levelSwitchingData);
491
+ // check if we need to load playlist for this level
492
+ const levelDetails = level.details;
493
+ if (!levelDetails || levelDetails.live) {
494
+ // level not retrieved yet, or live playlist we need to (re)load it
495
+ const hlsUrlParameters = this.switchParams(
496
+ level.uri,
497
+ lastLevel?.details,
498
+ levelDetails,
499
+ );
500
+ this.loadPlaylist(hlsUrlParameters);
501
+ }
502
+ }
503
+
504
+ get manualLevel(): number {
505
+ return this.manualLevelIndex;
506
+ }
507
+
508
+ set manualLevel(newLevel) {
509
+ this.manualLevelIndex = newLevel;
510
+ if (this._startLevel === undefined) {
511
+ this._startLevel = newLevel;
512
+ }
513
+
514
+ if (newLevel !== -1) {
515
+ this.level = newLevel;
516
+ }
517
+ }
518
+
519
+ get firstLevel(): number {
520
+ return this._firstLevel;
521
+ }
522
+
523
+ set firstLevel(newLevel) {
524
+ this._firstLevel = newLevel;
525
+ }
526
+
527
+ get startLevel(): number {
528
+ // Setting hls.startLevel (this._startLevel) overrides config.startLevel
529
+ if (this._startLevel === undefined) {
530
+ const configStartLevel = this.hls.config.startLevel;
531
+ if (configStartLevel !== undefined) {
532
+ return configStartLevel;
533
+ }
534
+ return this.hls.firstAutoLevel;
535
+ }
536
+ return this._startLevel;
537
+ }
538
+
539
+ set startLevel(newLevel: number) {
540
+ this._startLevel = newLevel;
541
+ }
542
+
543
+ get pathways(): string[] {
544
+ if (this.steering) {
545
+ return this.steering.pathways();
546
+ }
547
+
548
+ return [];
549
+ }
550
+
551
+ get pathwayPriority(): string[] | null {
552
+ if (this.steering) {
553
+ return this.steering.pathwayPriority;
554
+ }
555
+
556
+ return null;
557
+ }
558
+
559
+ set pathwayPriority(pathwayPriority: string[]) {
560
+ if (this.steering) {
561
+ const pathwaysList = this.steering.pathways();
562
+ const filteredPathwayPriority = pathwayPriority.filter((pathwayId) => {
563
+ return pathwaysList.indexOf(pathwayId) !== -1;
564
+ });
565
+ if (pathwayPriority.length < 1) {
566
+ this.warn(
567
+ `pathwayPriority ${pathwayPriority} should contain at least one pathway from list: ${pathwaysList}`,
568
+ );
569
+ return;
570
+ }
571
+ this.steering.pathwayPriority = filteredPathwayPriority;
572
+ }
573
+ }
574
+
575
+ protected onError(event: Events.ERROR, data: ErrorData) {
576
+ if (data.fatal || !data.context) {
577
+ return;
578
+ }
579
+
580
+ if (
581
+ data.context.type === PlaylistContextType.LEVEL &&
582
+ data.context.level === this.level
583
+ ) {
584
+ this.checkRetry(data);
585
+ }
586
+ }
587
+
588
+ // reset errors on the successful load of a fragment
589
+ protected onFragBuffered(
590
+ event: Events.FRAG_BUFFERED,
591
+ { frag }: FragBufferedData,
592
+ ) {
593
+ if (frag !== undefined && frag.type === PlaylistLevelType.MAIN) {
594
+ const el = frag.elementaryStreams;
595
+ if (!Object.keys(el).some((type) => !!el[type])) {
596
+ return;
597
+ }
598
+ const level = this._levels[frag.level];
599
+ if (level?.loadError) {
600
+ this.log(
601
+ `Resetting level error count of ${level.loadError} on frag buffered`,
602
+ );
603
+ level.loadError = 0;
604
+ }
605
+ }
606
+ }
607
+
608
+ protected onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
609
+ const { level, details } = data;
610
+ const curLevel = data.levelInfo;
611
+
612
+ if (!curLevel) {
613
+ this.warn(`Invalid level index ${level}`);
614
+ if (data.deliveryDirectives?.skip) {
615
+ details.deltaUpdateFailed = true;
616
+ }
617
+ return;
618
+ }
619
+
620
+ // only process level loaded events matching with expected level or prior to switch when media playlist is loaded directly
621
+ if (curLevel === this.currentLevel || data.withoutMultiVariant) {
622
+ // reset level load error counter on successful level loaded only if there is no issues with fragments
623
+ if (curLevel.fragmentError === 0) {
624
+ curLevel.loadError = 0;
625
+ }
626
+ // Ignore matching details populated by loading a Media Playlist directly
627
+ let previousDetails = curLevel.details;
628
+ if (previousDetails === data.details && previousDetails.advanced) {
629
+ previousDetails = undefined;
630
+ }
631
+
632
+ this.playlistLoaded(level, data, previousDetails);
633
+ } else if (data.deliveryDirectives?.skip) {
634
+ // received a delta playlist update that cannot be merged
635
+ details.deltaUpdateFailed = true;
636
+ }
637
+ }
638
+
639
+ protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters) {
640
+ super.loadPlaylist();
641
+ if (this.shouldLoadPlaylist(this.currentLevel)) {
642
+ this.scheduleLoading(this.currentLevel, hlsUrlParameters);
643
+ }
644
+ }
645
+
646
+ protected loadingPlaylist(
647
+ currentLevel: Level,
648
+ hlsUrlParameters: HlsUrlParameters | undefined,
649
+ ) {
650
+ super.loadingPlaylist(currentLevel, hlsUrlParameters);
651
+ const url = this.getUrlWithDirectives(currentLevel.uri, hlsUrlParameters);
652
+ const currentLevelIndex = this.currentLevelIndex;
653
+ const pathwayId = currentLevel.attrs['PATHWAY-ID'];
654
+ const details = currentLevel.details;
655
+ const age = details?.age;
656
+ this.log(
657
+ `Loading level index ${currentLevelIndex}${
658
+ hlsUrlParameters?.msn !== undefined
659
+ ? ' at sn ' + hlsUrlParameters.msn + ' part ' + hlsUrlParameters.part
660
+ : ''
661
+ }${pathwayId ? ' Pathway ' + pathwayId : ''}${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type || '' : '') : ''} ${url}`,
662
+ );
663
+
664
+ this.hls.trigger(Events.LEVEL_LOADING, {
665
+ url,
666
+ level: currentLevelIndex,
667
+ levelInfo: currentLevel,
668
+ pathwayId: currentLevel.attrs['PATHWAY-ID'],
669
+ id: 0, // Deprecated Level urlId
670
+ deliveryDirectives: hlsUrlParameters || null,
671
+ });
672
+ }
673
+
674
+ get nextLoadLevel() {
675
+ if (this.manualLevelIndex !== -1) {
676
+ return this.manualLevelIndex;
677
+ } else {
678
+ return this.hls.nextAutoLevel;
679
+ }
680
+ }
681
+
682
+ set nextLoadLevel(nextLevel) {
683
+ const currentLevel = this.currentLevelIndex;
684
+ const isABRSwitch = this.manualLevelIndex === -1;
685
+ if (isABRSwitch && nextLevel !== currentLevel && nextLevel !== -1) {
686
+ const abrSwitchInterval = this.hls.config.abrSwitchInterval;
687
+ if (abrSwitchInterval > 0) {
688
+ const now = performance.now();
689
+ const delta = now - this.lastABRSwitchTime;
690
+ const intervalMs = abrSwitchInterval * 1000;
691
+ if (this.lastABRSwitchTime > -1 && delta < intervalMs) {
692
+ this.warn(
693
+ `Preventing ABR level switch: ${currentLevel} -> ${nextLevel} (${Math.round(delta)}ms < ${intervalMs}ms / ${abrSwitchInterval}s)`,
694
+ );
695
+ return;
696
+ }
697
+
698
+ this.lastABRSwitchTime = now;
699
+ this.log(
700
+ `Allowing ABR level switch: ${currentLevel} -> ${nextLevel} (${Math.round(delta)}ms >= ${intervalMs}ms / ${abrSwitchInterval}s)`,
701
+ );
702
+ }
703
+ }
704
+
705
+ this.level = nextLevel;
706
+ if (this.manualLevelIndex === -1) {
707
+ this.hls.nextAutoLevel = nextLevel;
708
+ }
709
+ }
710
+
711
+ removeLevel(levelIndex: number) {
712
+ if (this._levels.length === 1) {
713
+ return;
714
+ }
715
+ const levels = this._levels.filter((level, index) => {
716
+ if (index !== levelIndex) {
717
+ return true;
718
+ }
719
+ if (this.steering) {
720
+ this.steering.removeLevel(level);
721
+ }
722
+ if (level === this.currentLevel) {
723
+ this.currentLevel = null;
724
+ this.currentLevelIndex = -1;
725
+ if (level.details) {
726
+ level.details.fragments.forEach((f) => (f.level = -1));
727
+ }
728
+ }
729
+ return false;
730
+ });
731
+ reassignFragmentLevelIndexes(levels);
732
+ this._levels = levels;
733
+ if (this.currentLevelIndex > -1 && this.currentLevel?.details) {
734
+ this.currentLevelIndex = this.currentLevel.details.fragments[0].level;
735
+ }
736
+ if (this.manualLevelIndex > -1) {
737
+ this.manualLevelIndex = this.currentLevelIndex;
738
+ }
739
+ const maxLevel = levels.length - 1;
740
+ this._firstLevel = Math.min(this._firstLevel, maxLevel);
741
+ if (this._startLevel) {
742
+ this._startLevel = Math.min(this._startLevel, maxLevel);
743
+ }
744
+ this.hls.trigger(Events.LEVELS_UPDATED, { levels });
745
+ }
746
+
747
+ private onLevelsUpdated(
748
+ event: Events.LEVELS_UPDATED,
749
+ { levels }: LevelsUpdatedData,
750
+ ) {
751
+ this._levels = levels;
752
+ }
753
+
754
+ public checkMaxAutoUpdated() {
755
+ const { autoLevelCapping, maxAutoLevel, maxHdcpLevel } = this.hls;
756
+ if (this._maxAutoLevel !== maxAutoLevel) {
757
+ this._maxAutoLevel = maxAutoLevel;
758
+ this.hls.trigger(Events.MAX_AUTO_LEVEL_UPDATED, {
759
+ autoLevelCapping,
760
+ levels: this.levels,
761
+ maxAutoLevel,
762
+ minAutoLevel: this.hls.minAutoLevel,
763
+ maxHdcpLevel,
764
+ });
765
+ }
766
+ }
767
+ }
768
+
769
+ function assignTrackIdsByGroup(tracks: MediaPlaylist[]): void {
770
+ const groups = {};
771
+ tracks.forEach((track) => {
772
+ const groupId = track.groupId || '';
773
+ track.id = groups[groupId] = groups[groupId] || 0;
774
+ groups[groupId]++;
775
+ });
776
+ }