@zenvor/hls.js 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.md +472 -0
- package/dist/hls-demo.js +26995 -0
- package/dist/hls-demo.js.map +1 -0
- package/dist/hls.d.mts +4204 -0
- package/dist/hls.d.ts +4204 -0
- package/dist/hls.js +40050 -0
- package/dist/hls.js.d.ts +4204 -0
- package/dist/hls.js.map +1 -0
- package/dist/hls.light.js +27145 -0
- package/dist/hls.light.js.map +1 -0
- package/dist/hls.light.min.js +2 -0
- package/dist/hls.light.min.js.map +1 -0
- package/dist/hls.light.mjs +26392 -0
- package/dist/hls.light.mjs.map +1 -0
- package/dist/hls.min.js +2 -0
- package/dist/hls.min.js.map +1 -0
- package/dist/hls.mjs +38956 -0
- package/dist/hls.mjs.map +1 -0
- package/dist/hls.worker.js +2 -0
- package/dist/hls.worker.js.map +1 -0
- package/package.json +143 -0
- package/src/config.ts +794 -0
- package/src/controller/abr-controller.ts +1019 -0
- package/src/controller/algo-data-controller.ts +794 -0
- package/src/controller/audio-stream-controller.ts +1099 -0
- package/src/controller/audio-track-controller.ts +454 -0
- package/src/controller/base-playlist-controller.ts +438 -0
- package/src/controller/base-stream-controller.ts +2526 -0
- package/src/controller/buffer-controller.ts +2015 -0
- package/src/controller/buffer-operation-queue.ts +159 -0
- package/src/controller/cap-level-controller.ts +367 -0
- package/src/controller/cmcd-controller.ts +422 -0
- package/src/controller/content-steering-controller.ts +622 -0
- package/src/controller/eme-controller.ts +1617 -0
- package/src/controller/error-controller.ts +627 -0
- package/src/controller/fps-controller.ts +146 -0
- package/src/controller/fragment-finders.ts +256 -0
- package/src/controller/fragment-tracker.ts +567 -0
- package/src/controller/gap-controller.ts +719 -0
- package/src/controller/id3-track-controller.ts +488 -0
- package/src/controller/interstitial-player.ts +302 -0
- package/src/controller/interstitials-controller.ts +2895 -0
- package/src/controller/interstitials-schedule.ts +698 -0
- package/src/controller/latency-controller.ts +294 -0
- package/src/controller/level-controller.ts +776 -0
- package/src/controller/stream-controller.ts +1597 -0
- package/src/controller/subtitle-stream-controller.ts +508 -0
- package/src/controller/subtitle-track-controller.ts +617 -0
- package/src/controller/timeline-controller.ts +677 -0
- package/src/crypt/aes-crypto.ts +36 -0
- package/src/crypt/aes-decryptor.ts +339 -0
- package/src/crypt/decrypter-aes-mode.ts +4 -0
- package/src/crypt/decrypter.ts +225 -0
- package/src/crypt/fast-aes-key.ts +39 -0
- package/src/define-plugin.d.ts +17 -0
- package/src/demux/audio/aacdemuxer.ts +126 -0
- package/src/demux/audio/ac3-demuxer.ts +170 -0
- package/src/demux/audio/adts.ts +249 -0
- package/src/demux/audio/base-audio-demuxer.ts +205 -0
- package/src/demux/audio/dolby.ts +21 -0
- package/src/demux/audio/mp3demuxer.ts +85 -0
- package/src/demux/audio/mpegaudio.ts +177 -0
- package/src/demux/chunk-cache.ts +42 -0
- package/src/demux/dummy-demuxed-track.ts +13 -0
- package/src/demux/inject-worker.ts +75 -0
- package/src/demux/mp4demuxer.ts +234 -0
- package/src/demux/sample-aes.ts +198 -0
- package/src/demux/transmuxer-interface.ts +449 -0
- package/src/demux/transmuxer-worker.ts +221 -0
- package/src/demux/transmuxer.ts +560 -0
- package/src/demux/tsdemuxer.ts +1256 -0
- package/src/demux/video/avc-video-parser.ts +401 -0
- package/src/demux/video/base-video-parser.ts +198 -0
- package/src/demux/video/exp-golomb.ts +153 -0
- package/src/demux/video/hevc-video-parser.ts +736 -0
- package/src/empty-es.js +5 -0
- package/src/empty.js +3 -0
- package/src/errors.ts +107 -0
- package/src/events.ts +548 -0
- package/src/exports-default.ts +3 -0
- package/src/exports-named.ts +81 -0
- package/src/hls.ts +1613 -0
- package/src/is-supported.ts +54 -0
- package/src/loader/date-range.ts +207 -0
- package/src/loader/fragment-loader.ts +403 -0
- package/src/loader/fragment.ts +487 -0
- package/src/loader/interstitial-asset-list.ts +162 -0
- package/src/loader/interstitial-event.ts +337 -0
- package/src/loader/key-loader.ts +439 -0
- package/src/loader/level-details.ts +203 -0
- package/src/loader/level-key.ts +259 -0
- package/src/loader/load-stats.ts +17 -0
- package/src/loader/m3u8-parser.ts +1072 -0
- package/src/loader/playlist-loader.ts +839 -0
- package/src/polyfills/number.ts +15 -0
- package/src/remux/aac-helper.ts +81 -0
- package/src/remux/mp4-generator.ts +1380 -0
- package/src/remux/mp4-remuxer.ts +1261 -0
- package/src/remux/passthrough-remuxer.ts +434 -0
- package/src/task-loop.ts +130 -0
- package/src/types/algo.ts +44 -0
- package/src/types/buffer.ts +105 -0
- package/src/types/component-api.ts +20 -0
- package/src/types/demuxer.ts +208 -0
- package/src/types/events.ts +574 -0
- package/src/types/fragment-tracker.ts +23 -0
- package/src/types/level.ts +268 -0
- package/src/types/loader.ts +198 -0
- package/src/types/media-playlist.ts +92 -0
- package/src/types/network-details.ts +3 -0
- package/src/types/remuxer.ts +104 -0
- package/src/types/track.ts +12 -0
- package/src/types/transmuxer.ts +46 -0
- package/src/types/tuples.ts +6 -0
- package/src/types/vtt.ts +11 -0
- package/src/utils/arrays.ts +22 -0
- package/src/utils/attr-list.ts +192 -0
- package/src/utils/binary-search.ts +46 -0
- package/src/utils/buffer-helper.ts +173 -0
- package/src/utils/cea-608-parser.ts +1413 -0
- package/src/utils/chunker.ts +41 -0
- package/src/utils/codecs.ts +314 -0
- package/src/utils/cues.ts +96 -0
- package/src/utils/discontinuities.ts +174 -0
- package/src/utils/encryption-methods-util.ts +21 -0
- package/src/utils/error-helper.ts +95 -0
- package/src/utils/event-listener-helper.ts +16 -0
- package/src/utils/ewma-bandwidth-estimator.ts +97 -0
- package/src/utils/ewma.ts +43 -0
- package/src/utils/fetch-loader.ts +331 -0
- package/src/utils/global.ts +2 -0
- package/src/utils/hash.ts +10 -0
- package/src/utils/hdr.ts +67 -0
- package/src/utils/hex.ts +32 -0
- package/src/utils/imsc1-ttml-parser.ts +261 -0
- package/src/utils/keysystem-util.ts +45 -0
- package/src/utils/level-helper.ts +629 -0
- package/src/utils/logger.ts +120 -0
- package/src/utils/media-option-attributes.ts +49 -0
- package/src/utils/mediacapabilities-helper.ts +301 -0
- package/src/utils/mediakeys-helper.ts +210 -0
- package/src/utils/mediasource-helper.ts +37 -0
- package/src/utils/mp4-tools.ts +1473 -0
- package/src/utils/number.ts +3 -0
- package/src/utils/numeric-encoding-utils.ts +26 -0
- package/src/utils/output-filter.ts +46 -0
- package/src/utils/rendition-helper.ts +505 -0
- package/src/utils/safe-json-stringify.ts +22 -0
- package/src/utils/texttrack-utils.ts +164 -0
- package/src/utils/time-ranges.ts +17 -0
- package/src/utils/timescale-conversion.ts +46 -0
- package/src/utils/utf8-utils.ts +18 -0
- package/src/utils/variable-substitution.ts +105 -0
- package/src/utils/vttcue.ts +384 -0
- package/src/utils/vttparser.ts +497 -0
- package/src/utils/webvtt-parser.ts +166 -0
- package/src/utils/xhr-loader.ts +337 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,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;
|