@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,1019 @@
1
+ import { ErrorDetails } from '../errors';
2
+ import { Events } from '../events';
3
+ import { PlaylistLevelType } from '../types/loader';
4
+ import EwmaBandWidthEstimator from '../utils/ewma-bandwidth-estimator';
5
+ import { Logger } from '../utils/logger';
6
+ import {
7
+ getMediaDecodingInfoPromise,
8
+ requiresMediaCapabilitiesDecodingInfo,
9
+ SUPPORTED_INFO_DEFAULT,
10
+ } from '../utils/mediacapabilities-helper';
11
+ import {
12
+ type AudioTracksByGroup,
13
+ type CodecSetTier,
14
+ getAudioTracksByGroup,
15
+ getCodecTiers,
16
+ getStartCodecTier,
17
+ } from '../utils/rendition-helper';
18
+ import { stringify } from '../utils/safe-json-stringify';
19
+ import type Hls from '../hls';
20
+ import type { Fragment } from '../loader/fragment';
21
+ import type { Part } from '../loader/fragment';
22
+ import type { AbrComponentAPI } from '../types/component-api';
23
+ import type {
24
+ ErrorData,
25
+ FragBufferedData,
26
+ FragLoadedData,
27
+ FragLoadingData,
28
+ LevelLoadedData,
29
+ LevelSwitchingData,
30
+ ManifestLoadingData,
31
+ } from '../types/events';
32
+ import type { Level, VideoRange } from '../types/level';
33
+ import type { LoaderStats } from '../types/loader';
34
+
35
+ class AbrController extends Logger implements AbrComponentAPI {
36
+ protected hls: Hls;
37
+ private lastLevelLoadSec: number = 0;
38
+ private lastLoadedFragLevel: number = -1;
39
+ private firstSelection: number = -1;
40
+ private _nextAutoLevel: number = -1;
41
+ private nextAutoLevelKey: string = '';
42
+ private audioTracksByGroup: AudioTracksByGroup | null = null;
43
+ private codecTiers: Record<string, CodecSetTier> | null = null;
44
+ private timer: number = -1;
45
+ private fragCurrent: Fragment | null = null;
46
+ private partCurrent: Part | null = null;
47
+ private bitrateTestDelay: number = 0;
48
+ private rebufferNotice: number = -1;
49
+ private supportedCache: Record<
50
+ string,
51
+ Promise<MediaCapabilitiesDecodingInfo>
52
+ > = {};
53
+
54
+ public bwEstimator: EwmaBandWidthEstimator;
55
+
56
+ constructor(hls: Hls) {
57
+ super('abr', hls.logger);
58
+ this.hls = hls;
59
+ this.bwEstimator = this.initEstimator();
60
+ this.registerListeners();
61
+ }
62
+
63
+ public resetEstimator(abrEwmaDefaultEstimate?: number) {
64
+ if (abrEwmaDefaultEstimate) {
65
+ this.log(`setting initial bwe to ${abrEwmaDefaultEstimate}`);
66
+ this.hls.config.abrEwmaDefaultEstimate = abrEwmaDefaultEstimate;
67
+ }
68
+ this.firstSelection = -1;
69
+ this.bwEstimator = this.initEstimator();
70
+ }
71
+
72
+ private initEstimator(): EwmaBandWidthEstimator {
73
+ const config = this.hls.config;
74
+ return new EwmaBandWidthEstimator(
75
+ config.abrEwmaSlowVoD,
76
+ config.abrEwmaFastVoD,
77
+ config.abrEwmaDefaultEstimate,
78
+ );
79
+ }
80
+
81
+ protected registerListeners() {
82
+ const { hls } = this;
83
+ hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
84
+ hls.on(Events.FRAG_LOADING, this.onFragLoading, this);
85
+ hls.on(Events.FRAG_LOADED, this.onFragLoaded, this);
86
+ hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
87
+ hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
88
+ hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
89
+ hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this);
90
+ hls.on(Events.MAX_AUTO_LEVEL_UPDATED, this.onMaxAutoLevelUpdated, this);
91
+ hls.on(Events.ERROR, this.onError, this);
92
+ }
93
+
94
+ protected unregisterListeners() {
95
+ const { hls } = this;
96
+ if (!hls) {
97
+ return;
98
+ }
99
+ hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
100
+ hls.off(Events.FRAG_LOADING, this.onFragLoading, this);
101
+ hls.off(Events.FRAG_LOADED, this.onFragLoaded, this);
102
+ hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
103
+ hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
104
+ hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
105
+ hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this);
106
+ hls.off(Events.MAX_AUTO_LEVEL_UPDATED, this.onMaxAutoLevelUpdated, this);
107
+ hls.off(Events.ERROR, this.onError, this);
108
+ }
109
+
110
+ public destroy() {
111
+ this.unregisterListeners();
112
+ this.clearTimer();
113
+ // @ts-ignore
114
+ this.hls = this._abandonRulesCheck = this.supportedCache = null;
115
+ this.fragCurrent = this.partCurrent = null;
116
+ }
117
+
118
+ protected onManifestLoading(
119
+ event: Events.MANIFEST_LOADING,
120
+ data: ManifestLoadingData,
121
+ ) {
122
+ this.lastLoadedFragLevel = -1;
123
+ this.firstSelection = -1;
124
+ this.lastLevelLoadSec = 0;
125
+ this.supportedCache = {};
126
+ this.fragCurrent = this.partCurrent = null;
127
+ this.onLevelsUpdated();
128
+ this.clearTimer();
129
+ }
130
+
131
+ private onLevelsUpdated() {
132
+ if (this.lastLoadedFragLevel > -1 && this.fragCurrent) {
133
+ this.lastLoadedFragLevel = this.fragCurrent.level;
134
+ }
135
+ this._nextAutoLevel = -1;
136
+ this.onMaxAutoLevelUpdated();
137
+ this.codecTiers = null;
138
+ this.audioTracksByGroup = null;
139
+ }
140
+
141
+ private onMaxAutoLevelUpdated() {
142
+ this.firstSelection = -1;
143
+ this.nextAutoLevelKey = '';
144
+ }
145
+
146
+ protected onFragLoading(event: Events.FRAG_LOADING, data: FragLoadingData) {
147
+ const frag = data.frag;
148
+ if (this.ignoreFragment(frag)) {
149
+ return;
150
+ }
151
+ if (!frag.bitrateTest) {
152
+ this.fragCurrent = frag;
153
+ this.partCurrent = data.part ?? null;
154
+ }
155
+ this.clearTimer();
156
+ this.timer = self.setInterval(this._abandonRulesCheck, 100);
157
+ }
158
+
159
+ protected onLevelSwitching(
160
+ event: Events.LEVEL_SWITCHING,
161
+ data: LevelSwitchingData,
162
+ ): void {
163
+ this.clearTimer();
164
+ }
165
+
166
+ protected onError(event: Events.ERROR, data: ErrorData) {
167
+ if (data.fatal) {
168
+ return;
169
+ }
170
+ switch (data.details) {
171
+ case ErrorDetails.BUFFER_ADD_CODEC_ERROR:
172
+ case ErrorDetails.BUFFER_APPEND_ERROR:
173
+ // Reset last loaded level so that a new selection can be made after calling recoverMediaError
174
+ this.lastLoadedFragLevel = -1;
175
+ this.firstSelection = -1;
176
+ break;
177
+ case ErrorDetails.FRAG_LOAD_TIMEOUT: {
178
+ const frag = data.frag;
179
+ const { fragCurrent, partCurrent: part } = this;
180
+ if (
181
+ frag &&
182
+ fragCurrent &&
183
+ frag.sn === fragCurrent.sn &&
184
+ frag.level === fragCurrent.level
185
+ ) {
186
+ const now = performance.now();
187
+ const stats: LoaderStats = part ? part.stats : frag.stats;
188
+ const timeLoading = now - stats.loading.start;
189
+ const ttfb = stats.loading.first
190
+ ? stats.loading.first - stats.loading.start
191
+ : -1;
192
+ const loadedFirstByte = stats.loaded && ttfb > -1;
193
+ if (loadedFirstByte) {
194
+ const ttfbEstimate = this.bwEstimator.getEstimateTTFB();
195
+ this.bwEstimator.sample(
196
+ timeLoading - Math.min(ttfbEstimate, ttfb),
197
+ stats.loaded,
198
+ );
199
+ } else {
200
+ this.bwEstimator.sampleTTFB(timeLoading);
201
+ }
202
+ }
203
+ break;
204
+ }
205
+ }
206
+ }
207
+
208
+ private getTimeToLoadFrag(
209
+ timeToFirstByteSec: number,
210
+ bandwidth: number,
211
+ fragSizeBits: number,
212
+ isSwitch: boolean,
213
+ ): number {
214
+ const fragLoadSec = timeToFirstByteSec + fragSizeBits / bandwidth;
215
+ const playlistLoadSec = isSwitch
216
+ ? timeToFirstByteSec + this.lastLevelLoadSec
217
+ : 0;
218
+ return fragLoadSec + playlistLoadSec;
219
+ }
220
+
221
+ protected onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
222
+ const config = this.hls.config;
223
+ const { loading } = data.stats;
224
+ const timeLoadingMs = loading.end - loading.first;
225
+ if (Number.isFinite(timeLoadingMs)) {
226
+ this.lastLevelLoadSec = timeLoadingMs / 1000;
227
+ }
228
+ if (data.details.live) {
229
+ this.bwEstimator.update(config.abrEwmaSlowLive, config.abrEwmaFastLive);
230
+ } else {
231
+ this.bwEstimator.update(config.abrEwmaSlowVoD, config.abrEwmaFastVoD);
232
+ }
233
+ if (this.timer > -1) {
234
+ this._abandonRulesCheck(data.levelInfo);
235
+ }
236
+ }
237
+
238
+ /*
239
+ This method monitors the download rate of the current fragment, and will downswitch if that fragment will not load
240
+ quickly enough to prevent underbuffering
241
+ */
242
+ private _abandonRulesCheck = (levelLoaded?: Level) => {
243
+ const { fragCurrent: frag, partCurrent: part, hls } = this;
244
+ const { autoLevelEnabled, media } = hls;
245
+ if (!frag || !media) {
246
+ return;
247
+ }
248
+
249
+ const now = performance.now();
250
+ const stats: LoaderStats = part ? part.stats : frag.stats;
251
+ const duration = part ? part.duration : frag.duration;
252
+ const timeLoading = now - stats.loading.start;
253
+ const minAutoLevel = hls.minAutoLevel;
254
+ const loadingFragForLevel = frag.level;
255
+ const currentAutoLevel = this._nextAutoLevel;
256
+ // If frag loading is aborted, complete, or from lowest level, stop timer and return
257
+ if (
258
+ stats.aborted ||
259
+ (stats.loaded && stats.loaded === stats.total) ||
260
+ loadingFragForLevel <= minAutoLevel
261
+ ) {
262
+ this.clearTimer();
263
+ // reset forced auto level value so that next level will be selected
264
+ this._nextAutoLevel = -1;
265
+ return;
266
+ }
267
+
268
+ // This check only runs if we're in ABR mode
269
+ if (!autoLevelEnabled) {
270
+ return;
271
+ }
272
+
273
+ // Must be loading/loaded a new level or be in a playing state
274
+ const fragBlockingSwitch =
275
+ currentAutoLevel > -1 && currentAutoLevel !== loadingFragForLevel;
276
+ const levelChange = !!levelLoaded || fragBlockingSwitch;
277
+ if (
278
+ !levelChange &&
279
+ (media.paused || !media.playbackRate || !media.readyState)
280
+ ) {
281
+ return;
282
+ }
283
+
284
+ const bufferInfo = hls.mainForwardBufferInfo;
285
+ if (!levelChange && bufferInfo === null) {
286
+ return;
287
+ }
288
+
289
+ const ttfbEstimate = this.bwEstimator.getEstimateTTFB();
290
+ const playbackRate = Math.abs(media.playbackRate);
291
+ // To maintain stable adaptive playback, only begin monitoring frag loading after half or more of its playback duration has passed
292
+ if (
293
+ timeLoading <=
294
+ Math.max(ttfbEstimate, 1000 * (duration / (playbackRate * 2)))
295
+ ) {
296
+ return;
297
+ }
298
+
299
+ // bufferStarvationDelay is an estimate of the amount time (in seconds) it will take to exhaust the buffer
300
+ const bufferStarvationDelay = bufferInfo
301
+ ? bufferInfo.len / playbackRate
302
+ : 0;
303
+ const ttfb = stats.loading.first
304
+ ? stats.loading.first - stats.loading.start
305
+ : -1;
306
+ const loadedFirstByte = stats.loaded && ttfb > -1;
307
+ const bwEstimate: number = this.getBwEstimate();
308
+ const levels = hls.levels;
309
+ const level = levels[loadingFragForLevel];
310
+ const expectedLen = Math.max(
311
+ stats.loaded,
312
+ Math.round((duration * (frag.bitrate || level.averageBitrate)) / 8),
313
+ );
314
+ let timeStreaming = loadedFirstByte ? timeLoading - ttfb : timeLoading;
315
+ if (timeStreaming < 1 && loadedFirstByte) {
316
+ timeStreaming = Math.min(timeLoading, (stats.loaded * 8) / bwEstimate);
317
+ }
318
+ const loadRate = loadedFirstByte
319
+ ? (stats.loaded * 1000) / timeStreaming
320
+ : 0;
321
+ // fragLoadDelay is an estimate of the time (in seconds) it will take to buffer the remainder of the fragment
322
+ const ttfbSeconds = ttfbEstimate / 1000;
323
+ const fragLoadedDelay = loadRate
324
+ ? (expectedLen - stats.loaded) / loadRate
325
+ : (expectedLen * 8) / bwEstimate + ttfbSeconds;
326
+ // Only downswitch if the time to finish loading the current fragment is greater than the amount of buffer left
327
+ if (fragLoadedDelay <= bufferStarvationDelay) {
328
+ return;
329
+ }
330
+
331
+ const bwe = loadRate ? loadRate * 8 : bwEstimate;
332
+ const live =
333
+ (levelLoaded?.details || this.hls.latestLevelDetails)?.live === true;
334
+ const abrBandWidthUpFactor = this.hls.config.abrBandWidthUpFactor;
335
+ let fragLevelNextLoadedDelay: number = Number.POSITIVE_INFINITY;
336
+ let nextLoadLevel: number;
337
+ // Iterate through lower level and try to find the largest one that avoids rebuffering
338
+ for (
339
+ nextLoadLevel = loadingFragForLevel - 1;
340
+ nextLoadLevel > minAutoLevel;
341
+ nextLoadLevel--
342
+ ) {
343
+ // compute time to load next fragment at lower level
344
+ // 8 = bits per byte (bps/Bps)
345
+ const levelNextBitrate = levels[nextLoadLevel].maxBitrate;
346
+ const requiresLevelLoad = !levels[nextLoadLevel].details || live;
347
+ fragLevelNextLoadedDelay = this.getTimeToLoadFrag(
348
+ ttfbSeconds,
349
+ bwe,
350
+ duration * levelNextBitrate,
351
+ requiresLevelLoad,
352
+ );
353
+ if (
354
+ fragLevelNextLoadedDelay <
355
+ Math.min(bufferStarvationDelay, duration + ttfbSeconds)
356
+ ) {
357
+ break;
358
+ }
359
+ }
360
+ // Only emergency switch down if it takes less time to load a new fragment at lowest level instead of continuing
361
+ // to load the current one
362
+ if (fragLevelNextLoadedDelay >= fragLoadedDelay) {
363
+ return;
364
+ }
365
+
366
+ // if estimated load time of new segment is completely unreasonable, ignore and do not emergency switch down
367
+ if (fragLevelNextLoadedDelay > duration * 10) {
368
+ return;
369
+ }
370
+ if (loadedFirstByte) {
371
+ // If there has been loading progress, sample bandwidth using loading time offset by minimum TTFB time
372
+ this.bwEstimator.sample(
373
+ timeLoading - Math.min(ttfbEstimate, ttfb),
374
+ stats.loaded,
375
+ );
376
+ } else {
377
+ // If there has been no loading progress, sample TTFB
378
+ this.bwEstimator.sampleTTFB(timeLoading);
379
+ }
380
+ const nextLoadLevelBitrate = levels[nextLoadLevel].maxBitrate;
381
+ if (this.getBwEstimate() * abrBandWidthUpFactor > nextLoadLevelBitrate) {
382
+ this.resetEstimator(nextLoadLevelBitrate);
383
+ }
384
+ const bestSwitchLevel = this.findBestLevel(
385
+ nextLoadLevelBitrate,
386
+ minAutoLevel,
387
+ nextLoadLevel,
388
+ 0,
389
+ bufferStarvationDelay,
390
+ 1,
391
+ 1,
392
+ );
393
+ if (bestSwitchLevel > -1) {
394
+ nextLoadLevel = bestSwitchLevel;
395
+ }
396
+
397
+ this.warn(`Fragment ${frag.sn}${
398
+ part ? ' part ' + part.index : ''
399
+ } of level ${loadingFragForLevel} is loading too slowly;
400
+ Fragment duration: ${frag.duration.toFixed(3)}
401
+ Time to underbuffer: ${bufferStarvationDelay.toFixed(3)} s
402
+ Estimated load time for current fragment: ${fragLoadedDelay.toFixed(3)} s
403
+ Estimated load time for down switch fragment: ${fragLevelNextLoadedDelay.toFixed(
404
+ 3,
405
+ )} s
406
+ TTFB estimate: ${ttfb | 0} ms
407
+ Current BW estimate: ${
408
+ Number.isFinite(bwEstimate) ? bwEstimate | 0 : 'Unknown'
409
+ } bps
410
+ New BW estimate: ${this.getBwEstimate() | 0} bps
411
+ Switching to level ${nextLoadLevel} @ ${nextLoadLevelBitrate | 0} bps`);
412
+
413
+ hls.nextLoadLevel = hls.nextAutoLevel = nextLoadLevel;
414
+
415
+ this.clearTimer();
416
+ const abortAndSwitch = () => {
417
+ // Are nextLoadLevel details available or is stream-controller still in "WAITING_LEVEL" state?
418
+ this.clearTimer();
419
+ if (
420
+ this.fragCurrent === frag &&
421
+ this.hls.loadLevel === nextLoadLevel &&
422
+ nextLoadLevel > 0
423
+ ) {
424
+ const bufferStarvationDelay = this.getStarvationDelay();
425
+ this
426
+ .warn(`Aborting inflight request ${nextLoadLevel > 0 ? 'and switching down' : ''}
427
+ Fragment duration: ${frag.duration.toFixed(3)} s
428
+ Time to underbuffer: ${bufferStarvationDelay.toFixed(3)} s`);
429
+ frag.abortRequests();
430
+ this.fragCurrent = this.partCurrent = null;
431
+ if (nextLoadLevel > minAutoLevel) {
432
+ let lowestSwitchLevel = this.findBestLevel(
433
+ this.hls.levels[minAutoLevel].bitrate,
434
+ minAutoLevel,
435
+ nextLoadLevel,
436
+ 0,
437
+ bufferStarvationDelay,
438
+ 1,
439
+ 1,
440
+ );
441
+ if (lowestSwitchLevel === -1) {
442
+ lowestSwitchLevel = minAutoLevel;
443
+ }
444
+ this.hls.nextLoadLevel = this.hls.nextAutoLevel = lowestSwitchLevel;
445
+ this.resetEstimator(this.hls.levels[lowestSwitchLevel].bitrate);
446
+ }
447
+ }
448
+ };
449
+ if (fragBlockingSwitch || fragLoadedDelay > fragLevelNextLoadedDelay * 2) {
450
+ abortAndSwitch();
451
+ } else {
452
+ this.timer = self.setInterval(
453
+ abortAndSwitch,
454
+ fragLevelNextLoadedDelay * 1000,
455
+ );
456
+ }
457
+
458
+ hls.trigger(Events.FRAG_LOAD_EMERGENCY_ABORTED, { frag, part, stats });
459
+ };
460
+
461
+ protected onFragLoaded(
462
+ event: Events.FRAG_LOADED,
463
+ { frag, part }: FragLoadedData,
464
+ ) {
465
+ const stats = part ? part.stats : frag.stats;
466
+ if (frag.type === PlaylistLevelType.MAIN) {
467
+ this.bwEstimator.sampleTTFB(stats.loading.first - stats.loading.start);
468
+ }
469
+ if (this.ignoreFragment(frag)) {
470
+ return;
471
+ }
472
+ // stop monitoring bw once frag loaded
473
+ this.clearTimer();
474
+ // reset forced auto level value so that next level will be selected
475
+ if (frag.level === this._nextAutoLevel) {
476
+ this._nextAutoLevel = -1;
477
+ }
478
+ this.firstSelection = -1;
479
+
480
+ // compute level average bitrate
481
+ if (this.hls.config.abrMaxWithRealBitrate) {
482
+ const duration = part ? part.duration : frag.duration;
483
+ const level = this.hls.levels[frag.level];
484
+ const loadedBytes =
485
+ (level.loaded ? level.loaded.bytes : 0) + stats.loaded;
486
+ const loadedDuration =
487
+ (level.loaded ? level.loaded.duration : 0) + duration;
488
+ level.loaded = { bytes: loadedBytes, duration: loadedDuration };
489
+ level.realBitrate = Math.round((8 * loadedBytes) / loadedDuration);
490
+ }
491
+ if (frag.bitrateTest) {
492
+ const fragBufferedData: FragBufferedData = {
493
+ stats,
494
+ frag,
495
+ part,
496
+ id: frag.type,
497
+ };
498
+ this.onFragBuffered(Events.FRAG_BUFFERED, fragBufferedData);
499
+ frag.bitrateTest = false;
500
+ } else {
501
+ // store level id after successful fragment load for playback
502
+ this.lastLoadedFragLevel = frag.level;
503
+ }
504
+ }
505
+
506
+ protected onFragBuffered(
507
+ event: Events.FRAG_BUFFERED,
508
+ data: FragBufferedData,
509
+ ) {
510
+ const { frag, part } = data;
511
+ const stats = part?.stats.loaded ? part.stats : frag.stats;
512
+
513
+ if (stats.aborted) {
514
+ return;
515
+ }
516
+ if (this.ignoreFragment(frag)) {
517
+ return;
518
+ }
519
+ // Use the difference between parsing and request instead of buffering and request to compute fragLoadingProcessing;
520
+ // rationale is that buffer appending only happens once media is attached. This can happen when config.startFragPrefetch
521
+ // is used. If we used buffering in that case, our BW estimate sample will be very large.
522
+ const processingMs =
523
+ stats.parsing.end -
524
+ stats.loading.start -
525
+ Math.min(
526
+ stats.loading.first - stats.loading.start,
527
+ this.bwEstimator.getEstimateTTFB(),
528
+ );
529
+ this.bwEstimator.sample(processingMs, stats.loaded);
530
+ stats.bwEstimate = this.getBwEstimate();
531
+ if (frag.bitrateTest) {
532
+ this.bitrateTestDelay = processingMs / 1000;
533
+ } else {
534
+ this.bitrateTestDelay = 0;
535
+ }
536
+ }
537
+
538
+ private ignoreFragment(frag: Fragment): boolean {
539
+ // Only count non-alt-audio frags which were actually buffered in our BW calculations
540
+ return frag.type !== PlaylistLevelType.MAIN || frag.sn === 'initSegment';
541
+ }
542
+
543
+ public clearTimer() {
544
+ if (this.timer > -1) {
545
+ self.clearInterval(this.timer);
546
+ this.timer = -1;
547
+ }
548
+ }
549
+
550
+ public get firstAutoLevel(): number {
551
+ const { maxAutoLevel, minAutoLevel } = this.hls;
552
+ const bwEstimate = this.getBwEstimate();
553
+ const maxStartDelay = this.hls.config.maxStarvationDelay;
554
+ const abrAutoLevel = this.findBestLevel(
555
+ bwEstimate,
556
+ minAutoLevel,
557
+ maxAutoLevel,
558
+ 0,
559
+ maxStartDelay,
560
+ 1,
561
+ 1,
562
+ );
563
+ if (abrAutoLevel > -1) {
564
+ return abrAutoLevel;
565
+ }
566
+ const firstLevel = this.hls.firstLevel;
567
+ const clamped = Math.min(Math.max(firstLevel, minAutoLevel), maxAutoLevel);
568
+ this.warn(
569
+ `Could not find best starting auto level. Defaulting to first in playlist ${firstLevel} clamped to ${clamped}`,
570
+ );
571
+ return clamped;
572
+ }
573
+
574
+ public get forcedAutoLevel(): number {
575
+ if (this.nextAutoLevelKey) {
576
+ return -1;
577
+ }
578
+ return this._nextAutoLevel;
579
+ }
580
+
581
+ // return next auto level
582
+ public get nextAutoLevel(): number {
583
+ const forcedAutoLevel = this.forcedAutoLevel;
584
+ const bwEstimator = this.bwEstimator;
585
+ const useEstimate = bwEstimator.canEstimate();
586
+ const loadedFirstFrag = this.lastLoadedFragLevel > -1;
587
+ // in case next auto level has been forced, and bw not available or not reliable, return forced value
588
+ if (
589
+ forcedAutoLevel !== -1 &&
590
+ (!useEstimate ||
591
+ !loadedFirstFrag ||
592
+ this.nextAutoLevelKey === this.getAutoLevelKey())
593
+ ) {
594
+ return forcedAutoLevel;
595
+ }
596
+
597
+ // compute next level using ABR logic
598
+ const nextABRAutoLevel =
599
+ useEstimate && loadedFirstFrag
600
+ ? this.getNextABRAutoLevel()
601
+ : this.firstAutoLevel;
602
+
603
+ // use forced auto level while it hasn't errored more than ABR selection
604
+ if (forcedAutoLevel !== -1) {
605
+ const levels = this.hls.levels;
606
+ if (
607
+ levels.length > Math.max(forcedAutoLevel, nextABRAutoLevel) &&
608
+ levels[forcedAutoLevel].loadError <= levels[nextABRAutoLevel].loadError
609
+ ) {
610
+ return forcedAutoLevel;
611
+ }
612
+ }
613
+
614
+ // save result until state has changed
615
+ this._nextAutoLevel = nextABRAutoLevel;
616
+ this.nextAutoLevelKey = this.getAutoLevelKey();
617
+
618
+ return nextABRAutoLevel;
619
+ }
620
+
621
+ private getAutoLevelKey(): string {
622
+ return `${this.getBwEstimate()}_${this.getStarvationDelay().toFixed(2)}`;
623
+ }
624
+
625
+ private getNextABRAutoLevel(): number {
626
+ const { fragCurrent, partCurrent, hls } = this;
627
+ if (hls.levels.length <= 1) {
628
+ return hls.loadLevel;
629
+ }
630
+ const { maxAutoLevel, config, minAutoLevel } = hls;
631
+ const currentFragDuration = partCurrent
632
+ ? partCurrent.duration
633
+ : fragCurrent
634
+ ? fragCurrent.duration
635
+ : 0;
636
+ const avgbw = this.getBwEstimate();
637
+ // bufferStarvationDelay is the wall-clock time left until the playback buffer is exhausted.
638
+ const bufferStarvationDelay = this.getStarvationDelay();
639
+
640
+ let bwFactor = config.abrBandWidthFactor;
641
+ let bwUpFactor = config.abrBandWidthUpFactor;
642
+
643
+ // First, look to see if we can find a level matching with our avg bandwidth AND that could also guarantee no rebuffering at all
644
+ if (bufferStarvationDelay) {
645
+ const bestLevel = this.findBestLevel(
646
+ avgbw,
647
+ minAutoLevel,
648
+ maxAutoLevel,
649
+ bufferStarvationDelay,
650
+ 0,
651
+ bwFactor,
652
+ bwUpFactor,
653
+ );
654
+ if (bestLevel >= 0) {
655
+ this.rebufferNotice = -1;
656
+ return bestLevel;
657
+ }
658
+ }
659
+ // not possible to get rid of rebuffering... try to find level that will guarantee less than maxStarvationDelay of rebuffering
660
+ let maxStarvationDelay = currentFragDuration
661
+ ? Math.min(currentFragDuration, config.maxStarvationDelay)
662
+ : config.maxStarvationDelay;
663
+
664
+ if (!bufferStarvationDelay) {
665
+ // in case buffer is empty, let's check if previous fragment was loaded to perform a bitrate test
666
+ const bitrateTestDelay = this.bitrateTestDelay;
667
+ if (bitrateTestDelay) {
668
+ // if it is the case, then we need to adjust our max starvation delay using maxLoadingDelay config value
669
+ // max video loading delay used in automatic start level selection :
670
+ // in that mode ABR controller will ensure that video loading time (ie the time to fetch the first fragment at lowest quality level +
671
+ // the time to fetch the fragment at the appropriate quality level is less than ```maxLoadingDelay``` )
672
+ // cap maxLoadingDelay and ensure it is not bigger 'than bitrate test' frag duration
673
+ const maxLoadingDelay = currentFragDuration
674
+ ? Math.min(currentFragDuration, config.maxLoadingDelay)
675
+ : config.maxLoadingDelay;
676
+ maxStarvationDelay = maxLoadingDelay - bitrateTestDelay;
677
+ this.info(
678
+ `bitrate test took ${Math.round(
679
+ 1000 * bitrateTestDelay,
680
+ )}ms, set first fragment max fetchDuration to ${Math.round(
681
+ 1000 * maxStarvationDelay,
682
+ )} ms`,
683
+ );
684
+ // don't use conservative factor on bitrate test
685
+ bwFactor = bwUpFactor = 1;
686
+ }
687
+ }
688
+ const bestLevel = this.findBestLevel(
689
+ avgbw,
690
+ minAutoLevel,
691
+ maxAutoLevel,
692
+ bufferStarvationDelay,
693
+ maxStarvationDelay,
694
+ bwFactor,
695
+ bwUpFactor,
696
+ );
697
+ if (this.rebufferNotice !== bestLevel) {
698
+ this.rebufferNotice = bestLevel;
699
+ this.info(
700
+ `${
701
+ bufferStarvationDelay ? 'rebuffering expected' : 'buffer is empty'
702
+ }, optimal quality level ${bestLevel}`,
703
+ );
704
+ }
705
+ if (bestLevel > -1) {
706
+ return bestLevel;
707
+ }
708
+ // If no matching level found, see if min auto level would be a better option
709
+ const minLevel = hls.levels[minAutoLevel];
710
+ const autoLevel = hls.loadLevelObj;
711
+ if (autoLevel && minLevel?.bitrate < autoLevel.bitrate) {
712
+ return minAutoLevel;
713
+ }
714
+ // or if bitrate is not lower, continue to use loadLevel
715
+ return hls.loadLevel;
716
+ }
717
+
718
+ private getStarvationDelay(): number {
719
+ const hls = this.hls;
720
+ const media = hls.media;
721
+ if (!media) {
722
+ return Infinity;
723
+ }
724
+ // playbackRate is the absolute value of the playback rate; if media.playbackRate is 0, we use 1 to load as
725
+ // if we're playing back at the normal rate.
726
+ const playbackRate =
727
+ media && media.playbackRate !== 0 ? Math.abs(media.playbackRate) : 1.0;
728
+ const bufferInfo = hls.mainForwardBufferInfo;
729
+ return (bufferInfo ? bufferInfo.len : 0) / playbackRate;
730
+ }
731
+
732
+ private getBwEstimate(): number {
733
+ return this.bwEstimator.canEstimate()
734
+ ? this.bwEstimator.getEstimate()
735
+ : this.hls.config.abrEwmaDefaultEstimate;
736
+ }
737
+
738
+ private findBestLevel(
739
+ currentBw: number,
740
+ minAutoLevel: number,
741
+ maxAutoLevel: number,
742
+ bufferStarvationDelay: number,
743
+ maxStarvationDelay: number,
744
+ bwFactor: number,
745
+ bwUpFactor: number,
746
+ ): number {
747
+ const maxFetchDuration: number = bufferStarvationDelay + maxStarvationDelay;
748
+ const lastLoadedFragLevel = this.lastLoadedFragLevel;
749
+ const selectionBaseLevel =
750
+ lastLoadedFragLevel === -1 ? this.hls.firstLevel : lastLoadedFragLevel;
751
+ const { fragCurrent, partCurrent } = this;
752
+ const { levels, allAudioTracks, loadLevel, config } = this.hls;
753
+ if (levels.length === 1) {
754
+ return 0;
755
+ }
756
+ const level = levels[selectionBaseLevel] as Level | undefined;
757
+ const live = !!this.hls.latestLevelDetails?.live;
758
+ const firstSelection = loadLevel === -1 || lastLoadedFragLevel === -1;
759
+ let currentCodecSet: string | undefined;
760
+ let currentVideoRange: VideoRange | undefined = 'SDR';
761
+ let currentFrameRate = level?.frameRate || 0;
762
+
763
+ const { audioPreference, videoPreference } = config;
764
+ const audioTracksByGroup =
765
+ this.audioTracksByGroup ||
766
+ (this.audioTracksByGroup = getAudioTracksByGroup(allAudioTracks));
767
+ let minStartIndex = -1;
768
+ if (firstSelection) {
769
+ if (this.firstSelection !== -1) {
770
+ return this.firstSelection;
771
+ }
772
+ const codecTiers =
773
+ this.codecTiers ||
774
+ (this.codecTiers = getCodecTiers(
775
+ levels,
776
+ audioTracksByGroup,
777
+ minAutoLevel,
778
+ maxAutoLevel,
779
+ ));
780
+ const startTier = getStartCodecTier(
781
+ codecTiers,
782
+ currentVideoRange,
783
+ currentBw,
784
+ audioPreference,
785
+ videoPreference,
786
+ );
787
+ const {
788
+ codecSet,
789
+ videoRanges,
790
+ minFramerate,
791
+ minBitrate,
792
+ minIndex,
793
+ preferHDR,
794
+ } = startTier;
795
+ minStartIndex = minIndex;
796
+ currentCodecSet = codecSet;
797
+ currentVideoRange = preferHDR
798
+ ? videoRanges[videoRanges.length - 1]
799
+ : videoRanges[0];
800
+ currentFrameRate = minFramerate;
801
+ currentBw = Math.max(currentBw, minBitrate);
802
+ this.log(`picked start tier ${stringify(startTier)}`);
803
+ } else {
804
+ currentCodecSet = level?.codecSet;
805
+ currentVideoRange = level?.videoRange;
806
+ }
807
+
808
+ const currentFragDuration = partCurrent
809
+ ? partCurrent.duration
810
+ : fragCurrent
811
+ ? fragCurrent.duration
812
+ : 0;
813
+
814
+ const ttfbEstimateSec = this.bwEstimator.getEstimateTTFB() / 1000;
815
+ const levelsSkipped: number[] = [];
816
+ for (let i = maxAutoLevel; i >= minAutoLevel; i--) {
817
+ const levelInfo = levels[i];
818
+ const upSwitch = i > selectionBaseLevel;
819
+ if (!levelInfo) {
820
+ continue;
821
+ }
822
+ if (
823
+ __USE_MEDIA_CAPABILITIES__ &&
824
+ config.useMediaCapabilities &&
825
+ !levelInfo.supportedResult &&
826
+ !levelInfo.supportedPromise
827
+ ) {
828
+ const mediaCapabilities = navigator.mediaCapabilities as
829
+ | MediaCapabilities
830
+ | undefined;
831
+ if (
832
+ typeof mediaCapabilities?.decodingInfo === 'function' &&
833
+ requiresMediaCapabilitiesDecodingInfo(
834
+ levelInfo,
835
+ audioTracksByGroup,
836
+ currentVideoRange,
837
+ currentFrameRate,
838
+ currentBw,
839
+ audioPreference,
840
+ )
841
+ ) {
842
+ levelInfo.supportedPromise = getMediaDecodingInfoPromise(
843
+ levelInfo,
844
+ audioTracksByGroup,
845
+ mediaCapabilities,
846
+ this.supportedCache,
847
+ );
848
+ levelInfo.supportedPromise
849
+ .then((decodingInfo) => {
850
+ if (!this.hls) {
851
+ return;
852
+ }
853
+ levelInfo.supportedResult = decodingInfo;
854
+ const levels = this.hls.levels;
855
+ const index = levels.indexOf(levelInfo);
856
+ if (decodingInfo.error) {
857
+ this.warn(
858
+ `MediaCapabilities decodingInfo error: "${
859
+ decodingInfo.error
860
+ }" for level ${index} ${stringify(decodingInfo)}`,
861
+ );
862
+ } else if (!decodingInfo.supported) {
863
+ this.warn(
864
+ `Unsupported MediaCapabilities decodingInfo result for level ${index} ${stringify(
865
+ decodingInfo,
866
+ )}`,
867
+ );
868
+ if (index > -1 && levels.length > 1) {
869
+ this.log(`Removing unsupported level ${index}`);
870
+ this.hls.removeLevel(index);
871
+ if (this.hls.loadLevel === -1) {
872
+ this.hls.nextLoadLevel = 0;
873
+ }
874
+ }
875
+ } else if (
876
+ decodingInfo.decodingInfoResults.some(
877
+ (info) =>
878
+ info.smooth === false || info.powerEfficient === false,
879
+ )
880
+ ) {
881
+ this.log(
882
+ `MediaCapabilities decodingInfo for level ${index} not smooth or powerEfficient: ${stringify(decodingInfo)}`,
883
+ );
884
+ }
885
+ })
886
+ .catch((error) => {
887
+ this.warn(
888
+ `Error handling MediaCapabilities decodingInfo: ${error}`,
889
+ );
890
+ });
891
+ } else {
892
+ levelInfo.supportedResult = SUPPORTED_INFO_DEFAULT;
893
+ }
894
+ }
895
+
896
+ // skip candidates which change codec-family or video-range,
897
+ // and which decrease or increase frame-rate for up and down-switch respectfully
898
+ if (
899
+ (currentCodecSet && levelInfo.codecSet !== currentCodecSet) ||
900
+ (currentVideoRange && levelInfo.videoRange !== currentVideoRange) ||
901
+ (upSwitch && currentFrameRate > levelInfo.frameRate) ||
902
+ (!upSwitch &&
903
+ currentFrameRate > 0 &&
904
+ currentFrameRate < levelInfo.frameRate) ||
905
+ levelInfo.supportedResult?.decodingInfoResults?.some(
906
+ (info) => info.smooth === false,
907
+ )
908
+ ) {
909
+ if (!firstSelection || i !== minStartIndex) {
910
+ levelsSkipped.push(i);
911
+ continue;
912
+ }
913
+ }
914
+
915
+ const levelDetails = levelInfo.details;
916
+ const avgDuration =
917
+ (partCurrent
918
+ ? levelDetails?.partTarget
919
+ : levelDetails?.averagetargetduration) || currentFragDuration;
920
+
921
+ let adjustedbw: number;
922
+ // follow algorithm captured from stagefright :
923
+ // https://android.googlesource.com/platform/frameworks/av/+/master/media/libstagefright/httplive/LiveSession.cpp
924
+ // Pick the highest bandwidth stream below or equal to estimated bandwidth.
925
+ // consider only 80% of the available bandwidth, but if we are switching up,
926
+ // be even more conservative (70%) to avoid overestimating and immediately
927
+ // switching back.
928
+ if (!upSwitch) {
929
+ adjustedbw = bwFactor * currentBw;
930
+ } else {
931
+ adjustedbw = bwUpFactor * currentBw;
932
+ }
933
+
934
+ // Use average bitrate when starvation delay (buffer length) is gt or eq two segment durations and rebuffering is not expected (maxStarvationDelay > 0)
935
+ const bitrate: number =
936
+ currentFragDuration &&
937
+ bufferStarvationDelay >= currentFragDuration * 2 &&
938
+ maxStarvationDelay === 0
939
+ ? levelInfo.averageBitrate
940
+ : levelInfo.maxBitrate;
941
+ const fetchDuration: number = this.getTimeToLoadFrag(
942
+ ttfbEstimateSec,
943
+ adjustedbw,
944
+ bitrate * avgDuration,
945
+ !levelDetails || levelDetails.live,
946
+ );
947
+
948
+ const canSwitchWithinTolerance =
949
+ // if adjusted bw is greater than level bitrate AND
950
+ adjustedbw >= bitrate &&
951
+ // no level change, or new level has no error history
952
+ (i === lastLoadedFragLevel ||
953
+ (levelInfo.loadError === 0 && levelInfo.fragmentError === 0)) &&
954
+ // fragment fetchDuration unknown OR live stream OR fragment fetchDuration less than max allowed fetch duration, then this level matches
955
+ // we don't account for max Fetch Duration for live streams, this is to avoid switching down when near the edge of live sliding window ...
956
+ // special case to support startLevel = -1 (bitrateTest) on live streams : in that case we should not exit loop so that findBestLevel will return -1
957
+ (fetchDuration <= ttfbEstimateSec ||
958
+ !Number.isFinite(fetchDuration) ||
959
+ (live && !this.bitrateTestDelay) ||
960
+ fetchDuration < maxFetchDuration);
961
+ if (canSwitchWithinTolerance) {
962
+ const forcedAutoLevel = this.forcedAutoLevel;
963
+ if (
964
+ i !== loadLevel &&
965
+ (forcedAutoLevel === -1 || forcedAutoLevel !== loadLevel)
966
+ ) {
967
+ if (levelsSkipped.length) {
968
+ this.trace(
969
+ `Skipped level(s) ${levelsSkipped.join(
970
+ ',',
971
+ )} of ${maxAutoLevel} max with CODECS and VIDEO-RANGE:"${
972
+ levels[levelsSkipped[0]].codecs
973
+ }" ${levels[levelsSkipped[0]].videoRange}; not compatible with "${
974
+ currentCodecSet
975
+ }" ${currentVideoRange}`,
976
+ );
977
+ }
978
+ this.info(
979
+ `switch candidate:${selectionBaseLevel}->${i} adjustedbw(${Math.round(
980
+ adjustedbw,
981
+ )})-bitrate=${Math.round(
982
+ adjustedbw - bitrate,
983
+ )} ttfb:${ttfbEstimateSec.toFixed(
984
+ 1,
985
+ )} avgDuration:${avgDuration.toFixed(
986
+ 1,
987
+ )} maxFetchDuration:${maxFetchDuration.toFixed(
988
+ 1,
989
+ )} fetchDuration:${fetchDuration.toFixed(
990
+ 1,
991
+ )} firstSelection:${firstSelection} codecSet:${levelInfo.codecSet} videoRange:${levelInfo.videoRange} hls.loadLevel:${loadLevel}`,
992
+ );
993
+ }
994
+ if (firstSelection) {
995
+ this.firstSelection = i;
996
+ }
997
+ // as we are looping from highest to lowest, this will return the best achievable quality level
998
+ return i;
999
+ }
1000
+ }
1001
+ // not enough time budget even with quality level 0 ... rebuffering might happen
1002
+ return -1;
1003
+ }
1004
+
1005
+ public set nextAutoLevel(nextLevel: number) {
1006
+ const value = this.deriveNextAutoLevel(nextLevel);
1007
+ if (this._nextAutoLevel !== value) {
1008
+ this.nextAutoLevelKey = '';
1009
+ this._nextAutoLevel = value;
1010
+ }
1011
+ }
1012
+
1013
+ protected deriveNextAutoLevel(nextLevel: number) {
1014
+ const { maxAutoLevel, minAutoLevel } = this.hls;
1015
+ return Math.min(Math.max(nextLevel, minAutoLevel), maxAutoLevel);
1016
+ }
1017
+ }
1018
+
1019
+ export default AbrController;