diff --git a/src/core/adaptive/__tests__/buffer_based_chooser.test.ts b/src/core/adaptive/__tests__/buffer_based_chooser.test.ts index 36d389ff60..dbe398866f 100644 --- a/src/core/adaptive/__tests__/buffer_based_chooser.test.ts +++ b/src/core/adaptive/__tests__/buffer_based_chooser.test.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { ScoreConfidenceLevel } from "../utils/representation_score_calculator"; + /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-var-requires */ @@ -47,12 +49,25 @@ describe("BufferBasedChooser", () => { bufferGap: 0, speed: 1, currentBitrate: undefined, - currentScore: 4, + currentScore: { score: 4, confidenceLevel: ScoreConfidenceLevel.LOW }, + })).toEqual(1); + expect(new BufferBasedChooser([1, 2, 3]).getEstimate({ + bufferGap: 0, + speed: 1, + currentBitrate: undefined, + currentScore: { score: 4, confidenceLevel: ScoreConfidenceLevel.HIGH }, + })).toEqual(1); + expect(new BufferBasedChooser([1, 2, 3]).getEstimate({ + bufferGap: 0, + speed: 1, + currentBitrate: undefined, + currentScore: { score: 1, confidenceLevel: ScoreConfidenceLevel.LOW }, })).toEqual(1); expect(new BufferBasedChooser([1, 2, 3]).getEstimate({ bufferGap: 0, speed: 1, - currentScore: 1, + currentBitrate: undefined, + currentScore: { score: 1, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(1); }); @@ -76,7 +91,7 @@ describe("BufferBasedChooser", () => { }); /* eslint-disable max-len */ - it("should go to the next bitrate if the current one is maintainable and we have more buffer than the next level", () => { + it("should not go to the next bitrate if we don't have a high enough maintainability score", () => { /* eslint-enable max-len */ const logger = { debug: jest.fn() }; jest.mock("../../../log", () => ({ __esModule: true as const, @@ -86,82 +101,82 @@ describe("BufferBasedChooser", () => { bufferGap: 16, speed: 1, currentBitrate: 10, - currentScore: 1.01, - })).toEqual(20); + currentScore: { score: 1.15, confidenceLevel: ScoreConfidenceLevel.LOW }, + })).toEqual(10); expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ bufferGap: 30, speed: 1, currentBitrate: 20, - currentScore: 1.01, - })).toEqual(40); + currentScore: { score: 1.15, confidenceLevel: ScoreConfidenceLevel.LOW }, + })).toEqual(20); expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ bufferGap: 30, speed: 1, currentBitrate: 20, - currentScore: 100, - })).toEqual(40); + currentScore: { score: 100, confidenceLevel: ScoreConfidenceLevel.LOW }, + })).toEqual(20); expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ bufferGap: 30, speed: 2, currentBitrate: 20, - currentScore: 2.1, - })).toEqual(40); + currentScore: { score: 2.30, confidenceLevel: ScoreConfidenceLevel.LOW }, + })).toEqual(20); expect(new BufferBasedChooser([10, 20, 20, 40]).getEstimate({ bufferGap: 30, speed: 2, currentBitrate: 20, - currentScore: 2.1, - })).toEqual(40); + currentScore: { score: 2.30, confidenceLevel: ScoreConfidenceLevel.LOW }, + })).toEqual(20); expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ bufferGap: 30, speed: 0, // 0 is a special case currentBitrate: 20, - currentScore: 100, - })).toEqual(40); + currentScore: { score: 100, confidenceLevel: ScoreConfidenceLevel.LOW }, + })).toEqual(20); }); /* eslint-disable max-len */ - it("should go to the next bitrate if the current one is maintainable and we have the buffer corresponding to the next level", () => { + it("should go to the next bitrate if the current one is maintainable and we have more buffer than the next level", () => { /* eslint-enable max-len */ const logger = { debug: jest.fn() }; jest.mock("../../../log", () => ({ __esModule: true as const, default: logger })); const BufferBasedChooser = jest.requireActual("../buffer_based_chooser").default; expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ - bufferGap: 15, + bufferGap: 16, speed: 1, currentBitrate: 10, - currentScore: 1.01, + currentScore: { score: 1.15, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(20); expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ - bufferGap: 20, + bufferGap: 30, speed: 1, currentBitrate: 20, - currentScore: 1.01, + currentScore: { score: 1.15, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(40); expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ - bufferGap: 20, + bufferGap: 30, speed: 1, currentBitrate: 20, - currentScore: 100, + currentScore: { score: 100, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(40); expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ - bufferGap: 20, + bufferGap: 30, speed: 2, currentBitrate: 20, - currentScore: 2.1, + currentScore: { score: 2.30, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(40); expect(new BufferBasedChooser([10, 20, 20, 40]).getEstimate({ - bufferGap: 20, + bufferGap: 30, speed: 2, currentBitrate: 20, - currentScore: 2.1, + currentScore: { score: 2.30, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(40); expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ - bufferGap: 20, + bufferGap: 30, speed: 0, // 0 is a special case currentBitrate: 20, - currentScore: 100, + currentScore: { score: 100, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(40); }); @@ -176,31 +191,31 @@ describe("BufferBasedChooser", () => { bufferGap: 6, speed: 1, currentBitrate: 10, - currentScore: 1.01, + currentScore: { score: 1.15, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(10); expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ - bufferGap: 10, + bufferGap: 13, speed: 1, currentBitrate: 20, - currentScore: 1.01, + currentScore: { score: 1.15, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(20); expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ - bufferGap: 10, + bufferGap: 13, speed: 1, currentBitrate: 20, - currentScore: 100, + currentScore: { score: 100, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(20); expect(new BufferBasedChooser([10, 20, 20, 40]).getEstimate({ - bufferGap: 10, + bufferGap: 13, speed: 1, currentBitrate: 20, - currentScore: 100, + currentScore: { score: 100, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(20); expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ - bufferGap: 10, + bufferGap: 13, speed: 2, currentBitrate: 20, - currentScore: 2.1, + currentScore: { score: 2.30, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(20); }); @@ -216,13 +231,13 @@ describe("BufferBasedChooser", () => { bufferGap: 100000000000, speed: 1, currentBitrate: 40, - currentScore: 1000000, + currentScore: { score: 1000000, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(40); expect(new BufferBasedChooser([10, 20, 40, 40]).getEstimate({ bufferGap: 100000000000, speed: 1, currentBitrate: 40, - currentScore: 1000000, + currentScore: { score: 1000000, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(40); }); @@ -237,31 +252,115 @@ describe("BufferBasedChooser", () => { bufferGap: 15, speed: 2, currentBitrate: 10, - currentScore: 1.01, + currentScore: { score: 2, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(10); expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ - bufferGap: 20, + bufferGap: 22, speed: 2, currentBitrate: 20, - currentScore: 1.01, + currentScore: { score: 2, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(20); expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ - bufferGap: 20, + bufferGap: 22, speed: 100, currentBitrate: 20, - currentScore: 100, + currentScore: { score: 100, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(20); expect(new BufferBasedChooser([10, 20, 20, 40]).getEstimate({ - bufferGap: 20, + bufferGap: 22, speed: 100, currentBitrate: 20, - currentScore: 100, + currentScore: { score: 100, confidenceLevel: ScoreConfidenceLevel.HIGH }, })).toEqual(20); expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ - bufferGap: 20, + bufferGap: 22, + speed: 3, + currentBitrate: 20, + currentScore: { score: 3, confidenceLevel: ScoreConfidenceLevel.HIGH }, + })).toEqual(20); + }); + + /* eslint-disable max-len */ + it("should lower bitrate if the current one is not maintainable due to the speed", () => { + /* eslint-enable max-len */ + const logger = { debug: jest.fn() }; + jest.mock("../../../log", () => ({ __esModule: true as const, + default: logger })); + const BufferBasedChooser = jest.requireActual("../buffer_based_chooser").default; + expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ + bufferGap: 15, + speed: 2, + currentBitrate: 10, + currentScore: { score: 1.9, confidenceLevel: ScoreConfidenceLevel.HIGH }, + })).toEqual(10); + expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ + bufferGap: 22, + speed: 2, + currentBitrate: 20, + currentScore: { score: 1.9, confidenceLevel: ScoreConfidenceLevel.HIGH }, + })).toEqual(10); + expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ + bufferGap: 22, + speed: 100, + currentBitrate: 20, + currentScore: { score: 99, confidenceLevel: ScoreConfidenceLevel.HIGH }, + })).toEqual(10); + expect(new BufferBasedChooser([10, 20, 20, 40]).getEstimate({ + bufferGap: 22, + speed: 100, + currentBitrate: 20, + currentScore: { score: 99, confidenceLevel: ScoreConfidenceLevel.HIGH }, + })).toEqual(10); + expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ + bufferGap: 22, + speed: 3, + currentBitrate: 20, + currentScore: { score: 2.5, confidenceLevel: ScoreConfidenceLevel.HIGH }, + })).toEqual(10); + }); + + /* eslint-disable max-len */ + it("should not lower bitrate if the current one is not maintainable due to the speed but confidence on the score is low", () => { + /* eslint-enable max-len */ + const logger = { debug: jest.fn() }; + jest.mock("../../../log", () => ({ __esModule: true as const, + default: logger })); + const BufferBasedChooser = jest.requireActual("../buffer_based_chooser").default; + expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ + bufferGap: 15, + speed: 2, + currentBitrate: 10, + currentScore: { score: 1.9, confidenceLevel: ScoreConfidenceLevel.LOW }, + })).toEqual(10); + expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ + bufferGap: 22, + speed: 2, + currentBitrate: 20, + currentScore: undefined, + })).toEqual(20); + expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ + bufferGap: 22, + speed: 2, + currentBitrate: 20, + currentScore: { score: 1.9, confidenceLevel: ScoreConfidenceLevel.LOW }, + })).toEqual(20); + expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ + bufferGap: 22, + speed: 100, + currentBitrate: 20, + currentScore: { score: 99, confidenceLevel: ScoreConfidenceLevel.LOW }, + })).toEqual(20); + expect(new BufferBasedChooser([10, 20, 20, 40]).getEstimate({ + bufferGap: 22, + speed: 100, + currentBitrate: 20, + currentScore: { score: 99, confidenceLevel: ScoreConfidenceLevel.LOW }, + })).toEqual(20); + expect(new BufferBasedChooser([10, 20, 40]).getEstimate({ + bufferGap: 22, speed: 3, currentBitrate: 20, - currentScore: 2.1, + currentScore: { score: 2.5, confidenceLevel: ScoreConfidenceLevel.LOW }, })).toEqual(20); }); diff --git a/src/core/adaptive/adaptive_representation_selector.ts b/src/core/adaptive/adaptive_representation_selector.ts index d3fb1175ec..5a311688a4 100644 --- a/src/core/adaptive/adaptive_representation_selector.ts +++ b/src/core/adaptive/adaptive_representation_selector.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import config from "../../config"; import log from "../../log"; import Manifest, { Adaptation, @@ -316,8 +317,7 @@ function getEstimateReference( const timeRanges = val.buffered; const bufferGap = getLeftSizeOfRange(timeRanges, position.last); const { representation } = val.content; - const scoreData = scoreCalculator.getEstimate(representation); - const currentScore = scoreData?.[0]; + const currentScore = scoreCalculator.getEstimate(representation); const currentBitrate = representation.bitrate; const observation = { bufferGap, currentBitrate, currentScore, speed }; currentBufferBasedEstimate = bufferBasedChooser.getEstimate(observation); @@ -362,11 +362,14 @@ function getEstimateReference( lastPlaybackObservation.speed : 1); - if (allowBufferBasedEstimates && bufferGap <= 5) { + const { ABR_ENTER_BUFFER_BASED_ALGO, + ABR_EXIT_BUFFER_BASED_ALGO } = config.getCurrent(); + + if (allowBufferBasedEstimates && bufferGap <= ABR_EXIT_BUFFER_BASED_ALGO) { allowBufferBasedEstimates = false; } else if (!allowBufferBasedEstimates && isFinite(bufferGap) && - bufferGap > 10) + bufferGap >= ABR_ENTER_BUFFER_BASED_ALGO) { allowBufferBasedEstimates = true; } diff --git a/src/core/adaptive/buffer_based_chooser.ts b/src/core/adaptive/buffer_based_chooser.ts index 7517a12a04..475350dff6 100644 --- a/src/core/adaptive/buffer_based_chooser.ts +++ b/src/core/adaptive/buffer_based_chooser.ts @@ -17,6 +17,54 @@ import log from "../../log"; import arrayFindIndex from "../../utils/array_find_index"; import getBufferLevels from "./utils/get_buffer_levels"; +import { + IRepresentationMaintainabilityScore, + ScoreConfidenceLevel, +} from "./utils/representation_score_calculator"; + +/** + * Minimum amount of time, in milliseconds, during which we are blocked from + * raising in quality after it had been considered as too high. + */ +const MINIMUM_BLOCK_RAISE_DELAY = 6000; + +/** + * Maximum amount of time, in milliseconds, during which we are blocked from + * raising in quality after it had been considered as too high. + */ +const MAXIMUM_BLOCK_RAISE_DELAY = 15000; + +/** + * Amount of time, in milliseconds, with which the blocking time in raising + * the quality will be incremented if the current quality estimate is seen + * as too unstable. + */ +const RAISE_BLOCKING_DELAY_INCREMENT = 3000; + +/** + * Amount of time, in milliseconds, with which the blocking time in raising + * the quality will be dcremented if the current quality estimate is seen + * as relatively stable, until `MINIMUM_BLOCK_RAISE_DELAY` is reached. + */ +const RAISE_BLOCKING_DELAY_DECREMENT = 1000; + +/** + * Amount of time, in milliseconds, after the "raise blocking delay" currently + * in place (during which it is forbidden to raise up in quality), during which + * we might want to raise the "raise blocking delay" if the last chosen quality + * seems unsuitable. + * + * For example, let's consider that the current raise blocking delay is at + * `4000`, or 4 seconds, and that this `STABILITY_CHECK_DELAY` is at `5000`, or + * 5 seconds. + * Here it means that if the estimated quality is found to be unsuitable less + * than 4+5 = 9 seconds after it last was, we will increment the raise blocking + * delay by `RAISE_BLOCKING_DELAY_INCREMENT` (unless `MAXIMUM_BLOCK_RAISE_DELAY` + * is reached). + * Else, if takes more than 9 seconds, the raise blocking delay might be + * decremented. + */ +const STABILITY_CHECK_DELAY = 9000; /** * Choose a bitrate based on the currently available buffer. @@ -29,18 +77,41 @@ import getBufferLevels from "./utils/get_buffer_levels"; * "maintanable" or not. * If so, we may switch to a better quality, or conversely to a worse quality. * + * It also rely on mechanisms to avoid fluctuating too much between qualities. + * * @class BufferBasedChooser */ export default class BufferBasedChooser { private _levelsMap : number[]; private _bitrates : number[]; + /** + * Laast timestamp, in terms of `performance.now`, at which the current + * quality was seen as too high by this algorithm. + * Begins at `undefined`. + */ + private _lastUnsuitableQualityTimestamp: number | undefined; + + /** + * After lowering in quality, we forbid raising during a set amount of time. + * This amount is adaptive may continue to raise if it seems that quality + * is switching too much between low and high qualities. + * + * `_blockRaiseDelay` represents this time in milliseconds. + */ + private _blockRaiseDelay: number; + /** * @param {Array.} bitrates */ constructor(bitrates : number[]) { - this._levelsMap = getBufferLevels(bitrates); + this._levelsMap = getBufferLevels(bitrates).map(bl => { + return bl + 4; // Add some buffer security as it will be used conjointly with + // other algorithms anyway + }); this._bitrates = bitrates; + this._lastUnsuitableQualityTimestamp = undefined; + this._blockRaiseDelay = MINIMUM_BLOCK_RAISE_DELAY; log.debug("ABR: Steps for buffer based chooser.", this._levelsMap.map((l, i) => `bufferLevel: ${l}, bitrate: ${bitrates[i]}`) .join(" ,")); @@ -59,43 +130,90 @@ export default class BufferBasedChooser { if (currentBitrate == null) { return bitrates[0]; } - const currentBitrateIndex = arrayFindIndex(bitrates, b => b === currentBitrate); + + let currentBitrateIndex = -1; + for (let i = 0; i < bitrates.length; i++) { + // There could be bitrate duplicates. Only take the last one to simplify + const bitrate = bitrates[i]; + if (bitrate === currentBitrate) { + currentBitrateIndex = i; + } else if (bitrate > currentBitrate) { + break; + } + } + if (currentBitrateIndex < 0 || bitrates.length !== bufferLevels.length) { log.error("ABR: Current Bitrate not found in the calculated levels"); return bitrates[0]; } let scaledScore : number|undefined; - if (currentScore != null) { - scaledScore = speed === 0 ? currentScore : (currentScore / speed); + if (currentScore !== undefined) { + scaledScore = speed === 0 ? currentScore.score : (currentScore.score / speed); } - if (scaledScore != null && scaledScore > 1) { - const currentBufferLevel = bufferLevels[currentBitrateIndex]; - const nextIndex = (() => { - for (let i = currentBitrateIndex + 1; i < bufferLevels.length; i++) { - if (bufferLevels[i] > currentBufferLevel) { - return i; - } - } - })(); - if (nextIndex != null) { - const nextBufferLevel = bufferLevels[nextIndex]; - if (bufferGap >= nextBufferLevel) { - return bitrates[nextIndex]; + const actualBufferGap = isFinite(bufferGap) ? + bufferGap : + 0; + + const now = performance.now(); + + if ( + actualBufferGap < bufferLevels[currentBitrateIndex] || + ( + scaledScore !== undefined && scaledScore < 1 && + currentScore?.confidenceLevel === ScoreConfidenceLevel.HIGH + ) + ) { + const timeSincePrev = this._lastUnsuitableQualityTimestamp === undefined ? + -1 : + now - this._lastUnsuitableQualityTimestamp; + if (timeSincePrev < this._blockRaiseDelay + STABILITY_CHECK_DELAY) { + const newDelay = this._blockRaiseDelay + RAISE_BLOCKING_DELAY_INCREMENT; + this._blockRaiseDelay = Math.min(newDelay, MAXIMUM_BLOCK_RAISE_DELAY); + log.debug("ABR: Incrementing blocking raise in BufferBasedChooser due " + + "to unstable quality", + this._blockRaiseDelay); + } else { + const newDelay = this._blockRaiseDelay - RAISE_BLOCKING_DELAY_DECREMENT; + this._blockRaiseDelay = Math.max(MINIMUM_BLOCK_RAISE_DELAY, newDelay); + log.debug("ABR: Lowering quality in BufferBasedChooser", this._blockRaiseDelay); + } + this._lastUnsuitableQualityTimestamp = now; + // Security if multiple bitrates are equal, we now take the first one + const baseIndex = arrayFindIndex(bitrates, (b) => b === currentBitrate); + for (let i = baseIndex - 1; i >= 0; i--) { + if (actualBufferGap >= bufferLevels[i]) { + return bitrates[i]; } } + return bitrates[0]; + } + + if ( + ( + this._lastUnsuitableQualityTimestamp !== undefined && + now - this._lastUnsuitableQualityTimestamp < this._blockRaiseDelay + ) || + scaledScore === undefined || scaledScore < 1.15 || + currentScore?.confidenceLevel !== ScoreConfidenceLevel.HIGH + ) { + return currentBitrate; } - if (scaledScore == null || scaledScore < 1.15) { - const currentBufferLevel = bufferLevels[currentBitrateIndex]; - if (bufferGap < currentBufferLevel) { - for (let i = currentBitrateIndex - 1; i >= 0; i--) { - if (bitrates[i] < currentBitrate) { - return bitrates[i]; - } + const currentBufferLevel = bufferLevels[currentBitrateIndex]; + const nextIndex = (() => { + for (let i = currentBitrateIndex + 1; i < bufferLevels.length; i++) { + if (bufferLevels[i] > currentBufferLevel) { + return i; } - return currentBitrate; + } + })(); + if (nextIndex !== undefined) { + const nextBufferLevel = bufferLevels[nextIndex]; + if (bufferGap >= nextBufferLevel) { + log.debug("ABR: Raising quality in BufferBasedChooser", bitrates[nextIndex]); + return bitrates[nextIndex]; } } return currentBitrate; @@ -113,7 +231,7 @@ export interface IBufferBasedChooserPlaybackObservation { /** The bitrate of the currently downloaded segments, in bps. */ currentBitrate? : number | undefined; /** The "maintainability score" of the currently downloaded segments. */ - currentScore? : number | undefined; + currentScore? : IRepresentationMaintainabilityScore | undefined; /** Playback rate wanted (e.g. `1` is regular playback, `2` is double speed etc.). */ speed : number; } diff --git a/src/core/adaptive/guess_based_chooser.ts b/src/core/adaptive/guess_based_chooser.ts index d8cd974f15..83cc1423dc 100644 --- a/src/core/adaptive/guess_based_chooser.ts +++ b/src/core/adaptive/guess_based_chooser.ts @@ -23,6 +23,7 @@ import LastEstimateStorage, { } from "./utils/last_estimate_storage"; import { IRequestInfo } from "./utils/pending_requests_store"; import RepresentationScoreCalculator, { + IRepresentationMaintainabilityScore, ScoreConfidenceLevel, } from "./utils/representation_score_calculator"; @@ -178,11 +179,11 @@ export default class GuessBasedChooser { private _canGuessHigher( bufferGap : number, speed : number, - [score, scoreConfidenceLevel] : [number, ScoreConfidenceLevel] + { score, confidenceLevel } : IRepresentationMaintainabilityScore ) : boolean { return isFinite(bufferGap) && bufferGap >= 2.5 && performance.now() > this._blockGuessesUntil && - scoreConfidenceLevel === ScoreConfidenceLevel.HIGH && + confidenceLevel === ScoreConfidenceLevel.HIGH && score / speed > 1.01; } @@ -197,13 +198,13 @@ export default class GuessBasedChooser { */ private _shouldStopGuess( lastGuess : Representation, - scoreData : [number, ScoreConfidenceLevel] | undefined, + scoreData : IRepresentationMaintainabilityScore | undefined, bufferGap : number, requests : IRequestInfo[] ) : boolean { - if (scoreData !== undefined && scoreData[0] < 1.01) { + if (scoreData !== undefined && scoreData.score < 1.01) { return true; - } else if ((scoreData === undefined || scoreData[0] < 1.2) && bufferGap < 0.6) { + } else if ((scoreData === undefined || scoreData.score < 1.2) && bufferGap < 0.6) { return true; } @@ -233,11 +234,11 @@ export default class GuessBasedChooser { private _isLastGuessValidated( lastGuess : Representation, incomingBestBitrate : number, - scoreData : [number, ScoreConfidenceLevel] | undefined + scoreData : IRepresentationMaintainabilityScore | undefined ) : boolean { if (scoreData !== undefined && - scoreData[1] === ScoreConfidenceLevel.HIGH && - scoreData[0] > 1.5) + scoreData.confidenceLevel === ScoreConfidenceLevel.HIGH && + scoreData.score > 1.5) { return true; } diff --git a/src/core/adaptive/network_analyzer.ts b/src/core/adaptive/network_analyzer.ts index e848265502..7b7bfc17e4 100644 --- a/src/core/adaptive/network_analyzer.ts +++ b/src/core/adaptive/network_analyzer.ts @@ -162,6 +162,13 @@ function estimateStarvationModeBitrate( const concernedRequest = concernedRequests[0]; const now = performance.now(); + + let minimumRequestTime = concernedRequest.content.segment.duration * 1.5; + minimumRequestTime = Math.min(minimumRequestTime, 3000); + minimumRequestTime = Math.max(minimumRequestTime, 12000); + if (now - concernedRequest.requestTimestamp < minimumRequestTime) { + return undefined; + } const lastProgressEvent = concernedRequest.progress.length > 0 ? concernedRequest.progress[concernedRequest.progress.length - 1] : undefined; @@ -178,7 +185,7 @@ function estimateStarvationModeBitrate( // Calculate estimated time spent rebuffering if we continue doing that request. const expectedRebufferingTime = remainingTime - (realBufferGap / speed); - if (expectedRebufferingTime > 2000) { + if (expectedRebufferingTime > 2500) { return bandwidthEstimate; } } @@ -402,10 +409,8 @@ export default class NetworkAnalyzer { ) : boolean { if (currentRepresentation === null) { return true; - } else if (bitrate === currentRepresentation.bitrate) { + } else if (bitrate >= currentRepresentation.bitrate) { return false; - } else if (bitrate > currentRepresentation.bitrate) { - return !this._inStarvationMode; } return shouldDirectlySwitchToLowBitrate(playbackInfo, currentRequests, diff --git a/src/core/adaptive/utils/representation_score_calculator.ts b/src/core/adaptive/utils/representation_score_calculator.ts index c3161d5451..3f4fd5166e 100644 --- a/src/core/adaptive/utils/representation_score_calculator.ts +++ b/src/core/adaptive/utils/representation_score_calculator.ts @@ -18,6 +18,26 @@ import log from "../../../log"; import { Representation } from "../../../manifest"; import EWMA from "./ewma"; +/** + * Object representing a maintainability score as calculated by the + * `RepresentationScoreCalculator`. + */ +export interface IRepresentationMaintainabilityScore { + /** + * Weighted mean of dividing the loaded segment's duration by the time to make + * their request. + */ + score : number; + + /** + * The confidence we have on the calculated `score` in reflecting a useful + * maintainability hint for the concerned Representation. + * + * Basically, the more segments have been loaded, the higher the confidence. + */ + confidenceLevel: ScoreConfidenceLevel; +} + /** * Calculate the "maintainability score" of a given Representation: * - A score higher than 1 means that the Representation can theorically @@ -107,7 +127,7 @@ export default class RepresentationScoreCalculator { */ public getEstimate( representation : Representation - ) : [number, ScoreConfidenceLevel] | undefined { + ) : IRepresentationMaintainabilityScore | undefined { if (this._currentRepresentationData === null || this._currentRepresentationData.representation.id !== representation.id) { @@ -119,7 +139,7 @@ export default class RepresentationScoreCalculator { loadedDuration >= 10 ? ScoreConfidenceLevel.HIGH : ScoreConfidenceLevel.LOW; - return [estimate, confidenceLevel]; + return { score: estimate, confidenceLevel }; } /** diff --git a/src/default_config.ts b/src/default_config.ts index d198f1e0de..2693d4b2f4 100644 --- a/src/default_config.ts +++ b/src/default_config.ts @@ -487,6 +487,33 @@ const DEFAULT_CONFIG = { */ SAMPLING_INTERVAL_NO_MEDIASOURCE: 500, + /** + * Amount of buffer to have ahead of the current position before we may + * consider buffer-based adaptive estimates, in seconds. + * + * For example setting it to `10` means that we need to have ten seconds of + * buffer ahead of the current position before relying on buffer-based + * adaptive estimates. + * + * To avoid getting in-and-out of the buffer-based logic all the time, it + * should be set higher than `ABR_EXIT_BUFFER_BASED_ALGO`. + */ + ABR_ENTER_BUFFER_BASED_ALGO: 10, + + /** + * Below this amount of buffer ahead of the current position, in seconds, we + * will stop using buffer-based estimate in our adaptive logic to select a + * quality. + * + * For example setting it to `5` means that if we have less than 5 seconds of + * buffer ahead of the current position, we should stop relying on + * buffer-based estimates to choose a quality. + * + * To avoid getting in-and-out of the buffer-based logic all the time, it + * should be set lower than `ABR_ENTER_BUFFER_BASED_ALGO`. + */ + ABR_EXIT_BUFFER_BASED_ALGO: 5, + /** * Minimum number of bytes sampled before we trust the estimate. * If we have not sampled much data, our estimate may not be accurate @@ -525,8 +552,8 @@ const DEFAULT_CONFIG = { * @type {Object} */ ABR_REGULAR_FACTOR: { - DEFAULT: 0.8, - LOW_LATENCY: 0.8, + DEFAULT: 0.72, + LOW_LATENCY: 0.72, }, /** diff --git a/src/utils/is_null_or_undefined.ts b/src/utils/is_null_or_undefined.ts index d167a632cd..d12ff87a68 100644 --- a/src/utils/is_null_or_undefined.ts +++ b/src/utils/is_null_or_undefined.ts @@ -20,7 +20,7 @@ * not always understood by newcomers to the code, and which can be overused when * only one of the possibility can arise. * @param {*} x - * @returns {*} + * @returns {boolean} */ export default function isNullOrUndefined(x : unknown) : x is null | undefined | void { return x === null || x === undefined;