@tianfeng98/hls.js
Version:
HLS.js is a JavaScript library that supports playing MPEG-TS and HEVC encoded HLS streams in browsers with support for MSE.
815 lines (760 loc) • 29 kB
text/typescript
import EwmaBandWidthEstimator from '../utils/ewma-bandwidth-estimator';
import { Events } from '../events';
import { ErrorDetails } from '../errors';
import { PlaylistLevelType } from '../types/loader';
import { logger } from '../utils/logger';
import {
SUPPORTED_INFO_DEFAULT,
getMediaDecodingInfoPromise,
requiresMediaCapabilitiesDecodingInfo,
} from '../utils/mediacapabilities-helper';
import {
getAudioTracksByGroup,
getCodecTiers,
getStartCodecTier,
type AudioTracksByGroup,
type CodecSetTier,
} from '../utils/rendition-helper';
import type { Fragment } from '../loader/fragment';
import type { Part } from '../loader/fragment';
import type { Level, VideoRange } from '../types/level';
import type { LoaderStats } from '../types/loader';
import type Hls from '../hls';
import type {
FragLoadingData,
FragLoadedData,
FragBufferedData,
LevelLoadedData,
LevelSwitchingData,
ManifestLoadingData,
ErrorData,
} from '../types/events';
import type { AbrComponentAPI } from '../types/component-api';
class AbrController implements AbrComponentAPI {
protected hls: Hls;
private lastLevelLoadSec: number = 0;
private lastLoadedFragLevel: number = -1;
private _nextAutoLevel: number = -1;
private nextAutoLevelKey: string = '';
private audioTracksByGroup: AudioTracksByGroup | null = null;
private codecTiers: Record<string, CodecSetTier> | null = null;
private timer: number = -1;
private onCheck: Function = this._abandonRulesCheck.bind(this);
private fragCurrent: Fragment | null = null;
private partCurrent: Part | null = null;
private bitrateTestDelay: number = 0;
public bwEstimator: EwmaBandWidthEstimator;
constructor(hls: Hls) {
this.hls = hls;
this.bwEstimator = this.initEstimator();
this.registerListeners();
}
public resetEstimator(abrEwmaDefaultEstimate?: number) {
if (abrEwmaDefaultEstimate) {
logger.log(`setting initial bwe to ${abrEwmaDefaultEstimate}`);
this.hls.config.abrEwmaDefaultEstimate = abrEwmaDefaultEstimate;
}
this.bwEstimator = this.initEstimator();
}
private initEstimator(): EwmaBandWidthEstimator {
const config = this.hls.config;
return new EwmaBandWidthEstimator(
config.abrEwmaSlowVoD,
config.abrEwmaFastVoD,
config.abrEwmaDefaultEstimate,
);
}
protected registerListeners() {
const { hls } = this;
hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
hls.on(Events.FRAG_LOADING, this.onFragLoading, this);
hls.on(Events.FRAG_LOADED, this.onFragLoaded, this);
hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this);
hls.on(Events.ERROR, this.onError, this);
}
protected unregisterListeners() {
const { hls } = this;
if (!hls) {
return;
}
hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
hls.off(Events.FRAG_LOADING, this.onFragLoading, this);
hls.off(Events.FRAG_LOADED, this.onFragLoaded, this);
hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this);
hls.off(Events.ERROR, this.onError, this);
}
public destroy() {
this.unregisterListeners();
this.clearTimer();
// @ts-ignore
this.hls = this.onCheck = null;
this.fragCurrent = this.partCurrent = null;
}
protected onManifestLoading(
event: Events.MANIFEST_LOADING,
data: ManifestLoadingData,
) {
this.lastLoadedFragLevel = -1;
this.lastLevelLoadSec = 0;
this.fragCurrent = this.partCurrent = null;
this.onLevelsUpdated();
this.clearTimer();
}
private onLevelsUpdated() {
if (this.lastLoadedFragLevel > -1 && this.fragCurrent) {
this.lastLoadedFragLevel = this.fragCurrent.level;
}
this._nextAutoLevel = -1;
this.nextAutoLevelKey = '';
this.codecTiers = null;
this.audioTracksByGroup = null;
}
protected onFragLoading(event: Events.FRAG_LOADING, data: FragLoadingData) {
const frag = data.frag;
if (this.ignoreFragment(frag)) {
return;
}
if (!frag.bitrateTest) {
this.fragCurrent = frag;
this.partCurrent = data.part ?? null;
}
this.clearTimer();
this.timer = self.setInterval(this.onCheck, 100);
}
protected onLevelSwitching(
event: Events.LEVEL_SWITCHING,
data: LevelSwitchingData,
): void {
this.clearTimer();
}
protected onError(event: Events.ERROR, data: ErrorData) {
if (data.fatal) {
return;
}
switch (data.details) {
case ErrorDetails.BUFFER_ADD_CODEC_ERROR:
case ErrorDetails.BUFFER_APPEND_ERROR:
// Reset last loaded level so that a new selection can be made after calling recoverMediaError
this.lastLoadedFragLevel = -1;
}
}
private getTimeToLoadFrag(
timeToFirstByteSec: number,
bandwidth: number,
fragSizeBits: number,
isSwitch: boolean,
): number {
const fragLoadSec = timeToFirstByteSec + fragSizeBits / bandwidth;
const playlistLoadSec = isSwitch ? this.lastLevelLoadSec : 0;
return fragLoadSec + playlistLoadSec;
}
protected onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
const config = this.hls.config;
const { loading } = data.stats;
const timeLoadingMs = loading.end - loading.start;
if (Number.isFinite(timeLoadingMs)) {
this.lastLevelLoadSec = timeLoadingMs / 1000;
}
if (data.details.live) {
this.bwEstimator.update(config.abrEwmaSlowLive, config.abrEwmaFastLive);
} else {
this.bwEstimator.update(config.abrEwmaSlowVoD, config.abrEwmaFastVoD);
}
}
/*
This method monitors the download rate of the current fragment, and will downswitch if that fragment will not load
quickly enough to prevent underbuffering
*/
private _abandonRulesCheck() {
const { fragCurrent: frag, partCurrent: part, hls } = this;
const { autoLevelEnabled, media } = hls;
if (!frag || !media) {
return;
}
const now = performance.now();
const stats: LoaderStats = part ? part.stats : frag.stats;
const duration = part ? part.duration : frag.duration;
const timeLoading = now - stats.loading.start;
const minAutoLevel = hls.minAutoLevel;
// If frag loading is aborted, complete, or from lowest level, stop timer and return
if (
stats.aborted ||
(stats.loaded && stats.loaded === stats.total) ||
frag.level <= minAutoLevel
) {
this.clearTimer();
// reset forced auto level value so that next level will be selected
this._nextAutoLevel = -1;
return;
}
// This check only runs if we're in ABR mode and actually playing
if (
!autoLevelEnabled ||
media.paused ||
!media.playbackRate ||
!media.readyState
) {
return;
}
const bufferInfo = hls.mainForwardBufferInfo;
if (bufferInfo === null) {
return;
}
const ttfbEstimate = this.bwEstimator.getEstimateTTFB();
const playbackRate = Math.abs(media.playbackRate);
// To maintain stable adaptive playback, only begin monitoring frag loading after half or more of its playback duration has passed
if (
timeLoading <=
Math.max(ttfbEstimate, 1000 * (duration / (playbackRate * 2)))
) {
return;
}
// bufferStarvationDelay is an estimate of the amount time (in seconds) it will take to exhaust the buffer
const bufferStarvationDelay = bufferInfo.len / playbackRate;
// Only downswitch if less than 2 fragment lengths are buffered
if (bufferStarvationDelay >= (2 * duration) / playbackRate) {
return;
}
const ttfb = stats.loading.first
? stats.loading.first - stats.loading.start
: -1;
const loadedFirstByte = stats.loaded && ttfb > -1;
const bwEstimate: number = this.getBwEstimate();
const levels = hls.levels;
const level = levels[frag.level];
const expectedLen =
stats.total ||
Math.max(stats.loaded, Math.round((duration * level.maxBitrate) / 8));
let timeStreaming = loadedFirstByte ? timeLoading - ttfb : timeLoading;
if (timeStreaming < 1 && loadedFirstByte) {
timeStreaming = Math.min(timeLoading, (stats.loaded * 8) / bwEstimate);
}
const loadRate = loadedFirstByte
? (stats.loaded * 1000) / timeStreaming
: 0;
// fragLoadDelay is an estimate of the time (in seconds) it will take to buffer the remainder of the fragment
const fragLoadedDelay = loadRate
? (expectedLen - stats.loaded) / loadRate
: (expectedLen * 8) / bwEstimate + ttfbEstimate / 1000;
// Only downswitch if the time to finish loading the current fragment is greater than the amount of buffer left
if (fragLoadedDelay <= bufferStarvationDelay) {
return;
}
const bwe = loadRate ? loadRate * 8 : bwEstimate;
let fragLevelNextLoadedDelay: number = Number.POSITIVE_INFINITY;
let nextLoadLevel: number;
// Iterate through lower level and try to find the largest one that avoids rebuffering
for (
nextLoadLevel = frag.level - 1;
nextLoadLevel > minAutoLevel;
nextLoadLevel--
) {
// compute time to load next fragment at lower level
// 8 = bits per byte (bps/Bps)
const levelNextBitrate = levels[nextLoadLevel].maxBitrate;
fragLevelNextLoadedDelay = this.getTimeToLoadFrag(
ttfbEstimate / 1000,
bwe,
duration * levelNextBitrate,
!levels[nextLoadLevel].details,
);
if (fragLevelNextLoadedDelay < bufferStarvationDelay) {
break;
}
}
// Only emergency switch down if it takes less time to load a new fragment at lowest level instead of continuing
// to load the current one
if (fragLevelNextLoadedDelay >= fragLoadedDelay) {
return;
}
// if estimated load time of new segment is completely unreasonable, ignore and do not emergency switch down
if (fragLevelNextLoadedDelay > duration * 10) {
return;
}
hls.nextLoadLevel = nextLoadLevel;
if (loadedFirstByte) {
// If there has been loading progress, sample bandwidth using loading time offset by minimum TTFB time
this.bwEstimator.sample(
timeLoading - Math.min(ttfbEstimate, ttfb),
stats.loaded,
);
} else {
// If there has been no loading progress, sample TTFB
this.bwEstimator.sampleTTFB(timeLoading);
}
this.clearTimer();
logger.warn(`[abr] Fragment ${frag.sn}${
part ? ' part ' + part.index : ''
} of level ${frag.level} is loading too slowly;
Time to underbuffer: ${bufferStarvationDelay.toFixed(3)} s
Estimated load time for current fragment: ${fragLoadedDelay.toFixed(3)} s
Estimated load time for down switch fragment: ${fragLevelNextLoadedDelay.toFixed(
3,
)} s
TTFB estimate: ${ttfb}
Current BW estimate: ${
Number.isFinite(bwEstimate) ? (bwEstimate / 1024).toFixed(3) : 'Unknown'
} Kb/s
New BW estimate: ${(this.getBwEstimate() / 1024).toFixed(3)} Kb/s
Aborting and switching to level ${nextLoadLevel}`);
if (frag.loader) {
this.fragCurrent = this.partCurrent = null;
frag.abortRequests();
}
hls.trigger(Events.FRAG_LOAD_EMERGENCY_ABORTED, { frag, part, stats });
}
protected onFragLoaded(
event: Events.FRAG_LOADED,
{ frag, part }: FragLoadedData,
) {
const stats = part ? part.stats : frag.stats;
if (frag.type === PlaylistLevelType.MAIN) {
this.bwEstimator.sampleTTFB(stats.loading.first - stats.loading.start);
}
if (this.ignoreFragment(frag)) {
return;
}
// stop monitoring bw once frag loaded
this.clearTimer();
// reset forced auto level value so that next level will be selected
this._nextAutoLevel = -1;
// compute level average bitrate
if (this.hls.config.abrMaxWithRealBitrate) {
const duration = part ? part.duration : frag.duration;
const level = this.hls.levels[frag.level];
const loadedBytes =
(level.loaded ? level.loaded.bytes : 0) + stats.loaded;
const loadedDuration =
(level.loaded ? level.loaded.duration : 0) + duration;
level.loaded = { bytes: loadedBytes, duration: loadedDuration };
level.realBitrate = Math.round((8 * loadedBytes) / loadedDuration);
}
if (frag.bitrateTest) {
const fragBufferedData: FragBufferedData = {
stats,
frag,
part,
id: frag.type,
};
this.onFragBuffered(Events.FRAG_BUFFERED, fragBufferedData);
frag.bitrateTest = false;
} else {
// store level id after successful fragment load for playback
this.lastLoadedFragLevel = frag.level;
}
}
protected onFragBuffered(
event: Events.FRAG_BUFFERED,
data: FragBufferedData,
) {
const { frag, part } = data;
const stats = part?.stats.loaded ? part.stats : frag.stats;
if (stats.aborted) {
return;
}
if (this.ignoreFragment(frag)) {
return;
}
// Use the difference between parsing and request instead of buffering and request to compute fragLoadingProcessing;
// rationale is that buffer appending only happens once media is attached. This can happen when config.startFragPrefetch
// is used. If we used buffering in that case, our BW estimate sample will be very large.
const processingMs =
stats.parsing.end -
stats.loading.start -
Math.min(
stats.loading.first - stats.loading.start,
this.bwEstimator.getEstimateTTFB(),
);
this.bwEstimator.sample(processingMs, stats.loaded);
stats.bwEstimate = this.getBwEstimate();
if (frag.bitrateTest) {
this.bitrateTestDelay = processingMs / 1000;
} else {
this.bitrateTestDelay = 0;
}
}
private ignoreFragment(frag: Fragment): boolean {
// Only count non-alt-audio frags which were actually buffered in our BW calculations
return frag.type !== PlaylistLevelType.MAIN || frag.sn === 'initSegment';
}
public clearTimer() {
if (this.timer > -1) {
self.clearInterval(this.timer);
this.timer = -1;
}
}
get firstAutoLevel(): number {
const { maxAutoLevel, minAutoLevel } = this.hls;
const maxStartDelay = this.hls.config.maxStarvationDelay;
const abrAutoLevel = this.findBestLevel(
this.getBwEstimate(),
minAutoLevel,
maxAutoLevel,
0,
maxStartDelay,
1,
1,
);
if (abrAutoLevel > -1) {
return abrAutoLevel;
}
const firstLevel = this.hls.firstLevel;
const clamped = Math.min(Math.max(firstLevel, minAutoLevel), maxAutoLevel);
logger.warn(
`[abr] Could not find best starting auto level. Defaulting to first in playlist ${firstLevel} clamped to ${clamped}`,
);
return clamped;
}
get forcedAutoLevel(): number {
if (this.nextAutoLevelKey) {
return -1;
}
return this._nextAutoLevel;
}
// return next auto level
get nextAutoLevel(): number {
const forcedAutoLevel = this._nextAutoLevel;
const bwEstimator = this.bwEstimator;
const useEstimate = bwEstimator.canEstimate();
const loadedFirstFrag = this.lastLoadedFragLevel > -1;
// in case next auto level has been forced, and bw not available or not reliable, return forced value
if (
forcedAutoLevel !== -1 &&
(!useEstimate ||
!loadedFirstFrag ||
this.nextAutoLevelKey === this.getAutoLevelKey())
) {
return forcedAutoLevel;
}
// compute next level using ABR logic
let nextABRAutoLevel =
useEstimate && loadedFirstFrag
? this.getNextABRAutoLevel()
: this.firstAutoLevel;
// use forced auto level when ABR selected level has errored
if (forcedAutoLevel !== -1) {
const levels = this.hls.levels;
if (
levels.length > Math.max(forcedAutoLevel, nextABRAutoLevel) &&
levels[forcedAutoLevel].loadError < levels[nextABRAutoLevel].loadError
) {
return forcedAutoLevel;
}
}
// if forced auto level has been defined, use it to cap ABR computed quality level
if (forcedAutoLevel !== -1) {
nextABRAutoLevel = Math.min(forcedAutoLevel, nextABRAutoLevel);
}
// save result until state has changed
this._nextAutoLevel = nextABRAutoLevel;
this.nextAutoLevelKey = this.getAutoLevelKey();
return nextABRAutoLevel;
}
private getAutoLevelKey(): string {
return `${this.getBwEstimate()}_${this.hls.mainForwardBufferInfo?.len}`;
}
private getNextABRAutoLevel(): number {
const { fragCurrent, partCurrent, hls } = this;
const { maxAutoLevel, config, minAutoLevel, media } = hls;
const currentFragDuration = partCurrent
? partCurrent.duration
: fragCurrent
? fragCurrent.duration
: 0;
// playbackRate is the absolute value of the playback rate; if media.playbackRate is 0, we use 1 to load as
// if we're playing back at the normal rate.
const playbackRate =
media && media.playbackRate !== 0 ? Math.abs(media.playbackRate) : 1.0;
const avgbw = this.getBwEstimate();
// bufferStarvationDelay is the wall-clock time left until the playback buffer is exhausted.
const bufferInfo = hls.mainForwardBufferInfo;
const bufferStarvationDelay =
(bufferInfo ? bufferInfo.len : 0) / playbackRate;
let bwFactor = config.abrBandWidthFactor;
let bwUpFactor = config.abrBandWidthUpFactor;
// First, look to see if we can find a level matching with our avg bandwidth AND that could also guarantee no rebuffering at all
if (bufferStarvationDelay) {
const bestLevel = this.findBestLevel(
avgbw,
minAutoLevel,
maxAutoLevel,
bufferStarvationDelay,
0,
bwFactor,
bwUpFactor,
);
if (bestLevel >= 0) {
return bestLevel;
}
}
// not possible to get rid of rebuffering... try to find level that will guarantee less than maxStarvationDelay of rebuffering
let maxStarvationDelay = currentFragDuration
? Math.min(currentFragDuration, config.maxStarvationDelay)
: config.maxStarvationDelay;
if (!bufferStarvationDelay) {
// in case buffer is empty, let's check if previous fragment was loaded to perform a bitrate test
const bitrateTestDelay = this.bitrateTestDelay;
if (bitrateTestDelay) {
// if it is the case, then we need to adjust our max starvation delay using maxLoadingDelay config value
// max video loading delay used in automatic start level selection :
// in that mode ABR controller will ensure that video loading time (ie the time to fetch the first fragment at lowest quality level +
// the time to fetch the fragment at the appropriate quality level is less than ```maxLoadingDelay``` )
// cap maxLoadingDelay and ensure it is not bigger 'than bitrate test' frag duration
const maxLoadingDelay = currentFragDuration
? Math.min(currentFragDuration, config.maxLoadingDelay)
: config.maxLoadingDelay;
maxStarvationDelay = maxLoadingDelay - bitrateTestDelay;
logger.info(
`[abr] bitrate test took ${Math.round(
1000 * bitrateTestDelay,
)}ms, set first fragment max fetchDuration to ${Math.round(
1000 * maxStarvationDelay,
)} ms`,
);
// don't use conservative factor on bitrate test
bwFactor = bwUpFactor = 1;
}
}
const bestLevel = this.findBestLevel(
avgbw,
minAutoLevel,
maxAutoLevel,
bufferStarvationDelay,
maxStarvationDelay,
bwFactor,
bwUpFactor,
);
logger.info(
`[abr] ${
bufferStarvationDelay ? 'rebuffering expected' : 'buffer is empty'
}, optimal quality level ${bestLevel}`,
);
if (bestLevel > -1) {
return bestLevel;
}
// If no matching level found, see if min auto level would be a better option
const minLevel = hls.levels[minAutoLevel];
const autoLevel = hls.levels[hls.loadLevel];
if (minLevel?.bitrate < autoLevel?.bitrate) {
return minAutoLevel;
}
// or if bitrate is not lower, continue to use loadLevel
return hls.loadLevel;
}
private getBwEstimate(): number {
return this.bwEstimator.canEstimate()
? this.bwEstimator.getEstimate()
: this.hls.config.abrEwmaDefaultEstimate;
}
private findBestLevel(
currentBw: number,
minAutoLevel: number,
maxAutoLevel: number,
bufferStarvationDelay: number,
maxStarvationDelay: number,
bwFactor: number,
bwUpFactor: number,
): number {
const maxFetchDuration: number = bufferStarvationDelay + maxStarvationDelay;
const lastLoadedFragLevel = this.lastLoadedFragLevel;
const selectionBaseLevel =
lastLoadedFragLevel === -1 ? this.hls.firstLevel : lastLoadedFragLevel;
const { fragCurrent, partCurrent } = this;
const { levels, allAudioTracks, loadLevel } = this.hls;
if (levels.length === 1) {
return 0;
}
const level: Level | undefined = levels[selectionBaseLevel];
const live = !!level?.details?.live;
const firstSelection = loadLevel === -1 || lastLoadedFragLevel === -1;
let currentCodecSet: string | undefined;
let currentVideoRange: VideoRange | undefined = 'SDR';
let currentFrameRate = level?.frameRate || 0;
const audioTracksByGroup =
this.audioTracksByGroup ||
(this.audioTracksByGroup = getAudioTracksByGroup(allAudioTracks));
if (firstSelection) {
const codecTiers =
this.codecTiers ||
(this.codecTiers = getCodecTiers(
levels,
audioTracksByGroup,
minAutoLevel,
maxAutoLevel,
));
const { codecSet, videoRange, minFramerate, minBitrate } =
getStartCodecTier(codecTiers, currentVideoRange, currentBw);
currentCodecSet = codecSet;
currentVideoRange = videoRange;
currentFrameRate = minFramerate;
currentBw = Math.max(currentBw, minBitrate);
} else {
currentCodecSet = level?.codecSet;
currentVideoRange = level?.videoRange;
}
const currentFragDuration = partCurrent
? partCurrent.duration
: fragCurrent
? fragCurrent.duration
: 0;
const ttfbEstimateSec = this.bwEstimator.getEstimateTTFB() / 1000;
const levelsSkipped: number[] = [];
for (let i = maxAutoLevel; i >= minAutoLevel; i--) {
const levelInfo = levels[i];
const upSwitch = i > selectionBaseLevel;
if (!levelInfo) {
continue;
}
if (
__USE_MEDIA_CAPABILITIES__ &&
this.hls.config.useMediaCapabilities &&
!levelInfo.supportedResult &&
!levelInfo.supportedPromise
) {
const mediaCapabilities = navigator.mediaCapabilities;
if (
requiresMediaCapabilitiesDecodingInfo(
levelInfo,
audioTracksByGroup,
mediaCapabilities,
currentVideoRange,
currentFrameRate,
currentBw,
)
) {
levelInfo.supportedPromise = getMediaDecodingInfoPromise(
levelInfo,
audioTracksByGroup,
mediaCapabilities,
);
levelInfo.supportedPromise.then((decodingInfo) => {
levelInfo.supportedResult = decodingInfo;
if (decodingInfo.error) {
logger.warn(
`[abr] MediaCapabilities decodingInfo error: "${
decodingInfo.error
}" for level ${i} ${JSON.stringify(decodingInfo)}`,
);
} else if (!decodingInfo.supported) {
logger.warn(
`[abr] Removing unsupported level ${i} after MediaCapabilities decodingInfo check failed ${JSON.stringify(
decodingInfo,
)}`,
);
if (i > 0) {
this.hls.removeLevel(i);
}
}
});
} else {
levelInfo.supportedResult = SUPPORTED_INFO_DEFAULT;
}
}
// skip candidates which change codec-family or video-range,
// and which decrease or increase frame-rate for up and down-switch respectfully
if (
(currentCodecSet && levelInfo.codecSet !== currentCodecSet) ||
(currentVideoRange && levelInfo.videoRange !== currentVideoRange) ||
(upSwitch && currentFrameRate > levelInfo.frameRate) ||
(!upSwitch &&
currentFrameRate > 0 &&
currentFrameRate < levelInfo.frameRate) ||
!levelInfo.supportedResult?.decodingInfoResults?.[0].smooth
) {
levelsSkipped.push(i);
continue;
}
const levelDetails = levelInfo.details;
const avgDuration =
(partCurrent
? levelDetails?.partTarget
: levelDetails?.averagetargetduration) || currentFragDuration;
let adjustedbw: number;
// follow algorithm captured from stagefright :
// https://android.googlesource.com/platform/frameworks/av/+/master/media/libstagefright/httplive/LiveSession.cpp
// Pick the highest bandwidth stream below or equal to estimated bandwidth.
// consider only 80% of the available bandwidth, but if we are switching up,
// be even more conservative (70%) to avoid overestimating and immediately
// switching back.
if (!upSwitch) {
adjustedbw = bwFactor * currentBw;
} else {
adjustedbw = bwUpFactor * currentBw;
}
// Use average bitrate when starvation delay (buffer length) is gt or eq two segment durations and rebuffering is not expected (maxStarvationDelay > 0)
const bitrate: number =
currentFragDuration &&
bufferStarvationDelay >= currentFragDuration * 2 &&
maxStarvationDelay === 0
? levels[i].averageBitrate
: levels[i].maxBitrate;
const fetchDuration: number = this.getTimeToLoadFrag(
ttfbEstimateSec,
adjustedbw,
bitrate * avgDuration,
levelDetails === undefined,
);
const canSwitchWithinTolerance =
// if adjusted bw is greater than level bitrate AND
adjustedbw >= bitrate &&
// no level change, or new level has no error history
(i === lastLoadedFragLevel ||
(levelInfo.loadError === 0 && levelInfo.fragmentError === 0)) &&
// fragment fetchDuration unknown OR live stream OR fragment fetchDuration less than max allowed fetch duration, then this level matches
// 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 ...
// special case to support startLevel = -1 (bitrateTest) on live streams : in that case we should not exit loop so that findBestLevel will return -1
(fetchDuration <= ttfbEstimateSec ||
!Number.isFinite(fetchDuration) ||
(live && !this.bitrateTestDelay) ||
fetchDuration < maxFetchDuration);
if (canSwitchWithinTolerance) {
if (i !== loadLevel) {
if (levelsSkipped.length) {
logger.trace(
`[abr] Skipped level(s) ${levelsSkipped.join(
',',
)} of ${maxAutoLevel} max with CODECS and VIDEO-RANGE:"${
levels[levelsSkipped[0]].codecs
}" ${levels[levelsSkipped[0]].videoRange}; not compatible with "${
level.codecs
}" ${currentVideoRange}`,
);
}
logger.info(
`[abr] switch candidate:${selectionBaseLevel}->${i} adjustedbw(${Math.round(
adjustedbw,
)})-bitrate=${Math.round(
adjustedbw - bitrate,
)} ttfb:${ttfbEstimateSec.toFixed(
1,
)} avgDuration:${avgDuration.toFixed(
1,
)} maxFetchDuration:${maxFetchDuration.toFixed(
1,
)} fetchDuration:${fetchDuration.toFixed(
1,
)} firstSelection:${firstSelection} codecSet:${currentCodecSet} videoRange:${currentVideoRange} hls.loadLevel:${loadLevel}`,
);
}
// as we are looping from highest to lowest, this will return the best achievable quality level
return i;
}
}
// not enough time budget even with quality level 0 ... rebuffering might happen
return -1;
}
set nextAutoLevel(nextLevel: number) {
const value = Math.max(this.hls.minAutoLevel, nextLevel);
if (this._nextAutoLevel != value) {
this.nextAutoLevelKey = '';
this._nextAutoLevel = value;
}
}
}
export default AbrController;