UNPKG

rx-player

Version:
274 lines (251 loc) 9.84 kB
/** * Copyright 2015 CANAL+ Group * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import log from "../../log"; import arrayFindIndex from "../../utils/array_find_index"; import isNullOrUndefined from "../../utils/is_null_or_undefined"; import getMonotonicTimeStamp from "../../utils/monotonic_timestamp"; import getBufferLevels from "./utils/get_buffer_levels"; import type { IRepresentationMaintainabilityScore } from "./utils/representation_score_calculator"; import { 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. * * This algorithm is based on a deviation of the BOLA algorithm. * It is a hybrid solution that also relies on a given bitrate's * "maintainability". * Each time a chunk is downloaded, from the ratio between the chunk duration * and chunk's request time, we can assume that the representation is * "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[]; /** * Current last best Representation's bitrate estimate made by the * `BufferBasedChooser` or `undefined` if it has no such guess for now. * * Might be updated each time `onAddedSegment` is called. */ private _currentEstimate: number | undefined; /** * Last monotonically-raising timestamp (as defined by the RxPlayer), 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.<number>} bitrates */ constructor(bitrates: number[]) { 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(" ,"), ); } /** * @param {Object} playbackObservation * @returns {number|undefined} */ public onAddedSegment( playbackObservation: IBufferBasedChooserPlaybackObservation, ): void { const bufferLevels = this._levelsMap; const bitrates = this._bitrates; const { bufferGap, currentBitrate, currentScore, speed } = playbackObservation; if (isNullOrUndefined(currentBitrate)) { this._currentEstimate = bitrates[0]; return; } 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.info("ABR", "Current Bitrate not found in the calculated levels"); this._currentEstimate = bitrates[0]; return; } let scaledScore: number | undefined; if (currentScore !== undefined) { scaledScore = speed === 0 ? currentScore.score : currentScore.score / speed; } const actualBufferGap = isFinite(bufferGap) ? bufferGap : 0; const now = getMonotonicTimeStamp(); 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; log.debug( "ABR", "Incrementing blocking raise in BufferBasedChooser due " + "to unstable quality", { prevDelay: this._blockRaiseDelay, newDelay }, ); this._blockRaiseDelay = Math.min(newDelay, MAXIMUM_BLOCK_RAISE_DELAY); } else { const newDelay = this._blockRaiseDelay - RAISE_BLOCKING_DELAY_DECREMENT; log.debug("ABR", "Lowering quality in BufferBasedChooser", { prevDelay: this._blockRaiseDelay, newDelay, }); this._blockRaiseDelay = Math.max(MINIMUM_BLOCK_RAISE_DELAY, newDelay); } 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]) { this._currentEstimate = bitrates[i]; return; } } this._currentEstimate = bitrates[0]; return; } if ( (this._lastUnsuitableQualityTimestamp !== undefined && now - this._lastUnsuitableQualityTimestamp < this._blockRaiseDelay) || scaledScore === undefined || scaledScore < 1.15 || currentScore?.confidenceLevel !== ScoreConfidenceLevel.HIGH ) { this._currentEstimate = currentBitrate; return; } const currentBufferLevel = bufferLevels[currentBitrateIndex]; const nextIndex = (() => { for (let i = currentBitrateIndex + 1; i < bufferLevels.length; i++) { if (bufferLevels[i] > currentBufferLevel) { return i; } } })(); if (nextIndex !== undefined) { const nextBufferLevel = bufferLevels[nextIndex]; if (bufferGap >= nextBufferLevel) { log.debug("ABR", "Raising quality in BufferBasedChooser", { bitrate: bitrates[nextIndex], }); this._currentEstimate = bitrates[nextIndex]; return; } } this._currentEstimate = currentBitrate; return; } /** * Returns the last best Representation's bitrate estimate made by the * `BufferBasedChooser` or `undefined` if it has no such guess for now. * * Might be updated after `onAddedSegment` is called. * * @returns {number|undefined} */ public getLastEstimate(): number | undefined { return this._currentEstimate; } } /** Playback observation needed by the `BufferBasedChooser`. */ export interface IBufferBasedChooserPlaybackObservation { /** * Difference in seconds between the current position and the next * non-buffered position in the buffer for the currently-considered * media type. */ bufferGap: number; /** The bitrate of the currently downloaded segments, in bps. */ currentBitrate?: number | undefined; /** The "maintainability score" of the currently downloaded segments. */ currentScore?: IRepresentationMaintainabilityScore | undefined; /** Playback rate wanted (e.g. `1` is regular playback, `2` is double speed etc.). */ speed: number; }