UNPKG

rx-player

Version:
356 lines (355 loc) 16.3 kB
"use strict"; /** * 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. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.estimateRequestBandwidth = estimateRequestBandwidth; var config_1 = require("../../config"); var log_1 = require("../../log"); var array_find_1 = require("../../utils/array_find"); var is_null_or_undefined_1 = require("../../utils/is_null_or_undefined"); var monotonic_timestamp_1 = require("../../utils/monotonic_timestamp"); var ewma_1 = require("./utils/ewma"); /** * Get pending segment request(s) starting with the asked segment position. * @param {Object} requests - Every requests pending, in a chronological * order in terms of segment time. * @param {number} neededPosition * @returns {Array.<Object>} */ function getConcernedRequests(requests, neededPosition) { /** Index of the request for the next needed segment, in `requests`. */ var nextSegmentIndex = -1; for (var i = 0; i < requests.length; i++) { var segment = requests[i].content.segment; if (segment.duration <= 0) { continue; } var segmentEnd = segment.time + segment.duration; if (!segment.complete) { if (i === requests.length - 1 && neededPosition - segment.time > -1.2) { nextSegmentIndex = i; break; } } if (segmentEnd > neededPosition && neededPosition - segment.time > -1.2) { nextSegmentIndex = i; break; } } if (nextSegmentIndex < 0) { // Not found return []; } var nextRequest = requests[nextSegmentIndex]; var segmentTime = nextRequest.content.segment.time; var filteredRequests = [nextRequest]; // Get the possibly multiple requests for that segment's position for (var i = nextSegmentIndex + 1; i < requests.length; i++) { if (requests[i].content.segment.time === segmentTime) { filteredRequests.push(requests[i]); } else { break; } } return filteredRequests; } /** * Estimate the __VERY__ recent bandwidth based on a single unfinished request. * Useful when the current bandwidth seemed to have fallen quickly. * * @param {Object} request * @returns {number|undefined} */ function estimateRequestBandwidth(request) { if (request.progress.length < 5) { // threshold from which we can consider // progress events reliably return undefined; } // try to infer quickly the current bitrate based on the // progress events var ewma1 = new ewma_1.default(2); var progress = request.progress; for (var i = 1; i < progress.length; i++) { var bytesDownloaded = progress[i].size - progress[i - 1].size; var timeElapsed = progress[i].timestamp - progress[i - 1].timestamp; var reqBitrate = (bytesDownloaded * 8) / (timeElapsed / 1000); ewma1.addSample(timeElapsed / 1000, reqBitrate); } return ewma1.getEstimate(); } /** * Estimate remaining time for a pending request from a progress event. * @param {Object} lastProgressEvent * @param {number} bandwidthEstimate * @returns {number} */ function estimateRemainingTime(lastProgressEvent, bandwidthEstimate) { var remainingData = (lastProgressEvent.totalSize - lastProgressEvent.size) * 8; return Math.max(remainingData / bandwidthEstimate, 0); } /** * Check if the request for the most needed segment is too slow. * If that's the case, re-calculate the bandwidth urgently based on * this single request. * @param {Object} pendingRequests - Every requests pending, in a chronological * order in terms of segment time. * @param {Object} playbackInfo - Information on the current playback. * @param {Object|null} currentRepresentation - The Representation being * presently being loaded. * @param {boolean} lowLatencyMode - If `true`, we're playing the content as a * low latency content - where requests might be pending when the segment is * still encoded. * @param {Number} lastEstimatedBitrate - Last bitrate estimate emitted. * @returns {Number|undefined} */ function estimateStarvationModeBitrate(pendingRequests, playbackInfo, currentRepresentation, lowLatencyMode, lastEstimatedBitrate) { if (lowLatencyMode) { // TODO Skip only for newer segments? return undefined; } var bufferGap = playbackInfo.bufferGap, speed = playbackInfo.speed, position = playbackInfo.position; var realBufferGap = isFinite(bufferGap) ? bufferGap : 0; var nextNeededPosition = position.getWanted() + realBufferGap; var concernedRequests = getConcernedRequests(pendingRequests, nextNeededPosition); if (concernedRequests.length !== 1) { // 0 == no request // 2+ == too complicated to calculate return undefined; } var concernedRequest = concernedRequests[0]; var now = (0, monotonic_timestamp_1.default)(); var minimumRequestTime = concernedRequest.content.segment.duration * 1.5; minimumRequestTime = Math.min(minimumRequestTime, 3000); minimumRequestTime = Math.max(minimumRequestTime, 12000); if (now - concernedRequest.requestTimestamp < minimumRequestTime) { return undefined; } var lastProgressEvent = concernedRequest.progress.length > 0 ? concernedRequest.progress[concernedRequest.progress.length - 1] : undefined; // first, try to do a quick estimate from progress events var bandwidthEstimate = estimateRequestBandwidth(concernedRequest); if (lastProgressEvent !== undefined && bandwidthEstimate !== undefined) { var remainingTime = estimateRemainingTime(lastProgressEvent, bandwidthEstimate); // if the remaining time does seem reliable if ((now - lastProgressEvent.timestamp) / 1000 <= remainingTime) { // Calculate estimated time spent rebuffering if we continue doing that request. var expectedRebufferingTime = remainingTime - realBufferGap / speed; if (expectedRebufferingTime > 2500) { return bandwidthEstimate; } } } if (!concernedRequest.content.segment.complete) { return undefined; } var chunkDuration = concernedRequest.content.segment.duration; var requestElapsedTime = (now - concernedRequest.requestTimestamp) / 1000; var reasonableElapsedTime = requestElapsedTime <= (chunkDuration * 1.5 + 2) / speed; if ((0, is_null_or_undefined_1.default)(currentRepresentation) || reasonableElapsedTime) { return undefined; } // calculate a reduced bitrate from the current one var factor = chunkDuration / requestElapsedTime; var reducedBitrate = currentRepresentation.bitrate * Math.min(0.7, factor); if (lastEstimatedBitrate === undefined || reducedBitrate < lastEstimatedBitrate) { return reducedBitrate; } } /** * Returns true if, based on the current requests, it seems that the ABR should * switch immediately if a lower bitrate is more adapted. * Returns false if it estimates that you have time before switching to a lower * bitrate. * @param {Object} playbackInfo - Information on the current playback. * @param {Object} requests - Every requests pending, in a chronological * order in terms of segment time. * @param {boolean} lowLatencyMode - If `true`, we're playing the content as a * low latency content, as close to the live edge as possible. * @returns {boolean} */ function shouldDirectlySwitchToLowBitrate(playbackInfo, requests, lowLatencyMode) { if (lowLatencyMode) { // TODO only when playing close to the live edge? return true; } var realBufferGap = isFinite(playbackInfo.bufferGap) ? playbackInfo.bufferGap : 0; var nextNeededPosition = playbackInfo.position.getWanted() + realBufferGap; var nextRequest = (0, array_find_1.default)(requests, function (_a) { var content = _a.content; return content.segment.duration > 0 && content.segment.time + content.segment.duration > nextNeededPosition; }); if (nextRequest === undefined) { return true; } var now = (0, monotonic_timestamp_1.default)(); var lastProgressEvent = nextRequest.progress.length > 0 ? nextRequest.progress[nextRequest.progress.length - 1] : undefined; // first, try to do a quick estimate from progress events var bandwidthEstimate = estimateRequestBandwidth(nextRequest); if (lastProgressEvent === undefined || bandwidthEstimate === undefined) { return true; } var remainingTime = estimateRemainingTime(lastProgressEvent, bandwidthEstimate); if ((now - lastProgressEvent.timestamp) / 1000 > remainingTime * 1.2) { return true; } var expectedRebufferingTime = remainingTime - realBufferGap / playbackInfo.speed; return expectedRebufferingTime > -1.5; } /** * Analyze the current network conditions and give a bandwidth estimate as well * as a maximum bitrate a Representation should be. * @class NetworkAnalyzer */ var NetworkAnalyzer = /** @class */ (function () { function NetworkAnalyzer(initialBitrate, lowLatencyMode) { var _a = config_1.default.getCurrent(), ABR_STARVATION_GAP = _a.ABR_STARVATION_GAP, OUT_OF_STARVATION_GAP = _a.OUT_OF_STARVATION_GAP, ABR_STARVATION_FACTOR = _a.ABR_STARVATION_FACTOR, ABR_REGULAR_FACTOR = _a.ABR_REGULAR_FACTOR; this._initialBitrate = initialBitrate; this._inStarvationMode = false; this._lowLatencyMode = lowLatencyMode; if (lowLatencyMode) { this._config = { starvationGap: ABR_STARVATION_GAP.LOW_LATENCY, outOfStarvationGap: OUT_OF_STARVATION_GAP.LOW_LATENCY, starvationBitrateFactor: ABR_STARVATION_FACTOR.LOW_LATENCY, regularBitrateFactor: ABR_REGULAR_FACTOR.LOW_LATENCY, }; } else { this._config = { starvationGap: ABR_STARVATION_GAP.DEFAULT, outOfStarvationGap: OUT_OF_STARVATION_GAP.DEFAULT, starvationBitrateFactor: ABR_STARVATION_FACTOR.DEFAULT, regularBitrateFactor: ABR_REGULAR_FACTOR.DEFAULT, }; } } /** * Gives an estimate of the current bandwidth and of the bitrate that should * be considered for chosing a `representation`. * This estimate is only based on network metrics. * @param {Object} playbackInfo - Gives current information about playback. * @param {Object} bandwidthEstimator - `BandwidthEstimator` allowing to * produce network bandwidth estimates. * @param {Object|null} currentRepresentation - The Representation currently * chosen. * `null` if no Representation has been chosen yet. * @param {Array.<Object>} currentRequests - All segment requests by segment's * start chronological order * @param {number|undefined} lastEstimatedBitrate - Bitrate emitted during the * last estimate. * @returns {Object} */ NetworkAnalyzer.prototype.getBandwidthEstimate = function (playbackInfo, bandwidthEstimator, currentRepresentation, currentRequests, lastEstimatedBitrate) { var newBitrateCeil; // bitrate ceil for the chosen Representation var bandwidthEstimate; var localConf = this._config; var bufferGap = playbackInfo.bufferGap, position = playbackInfo.position, duration = playbackInfo.duration; var realBufferGap = isFinite(bufferGap) ? bufferGap : 0; var ABR_STARVATION_DURATION_DELTA = config_1.default.getCurrent().ABR_STARVATION_DURATION_DELTA; // check if should get in/out of starvation mode if (isNaN(duration) || realBufferGap + position.getWanted() < duration - ABR_STARVATION_DURATION_DELTA) { if (!this._inStarvationMode && realBufferGap <= localConf.starvationGap) { log_1.default.info("ABR", "enter starvation mode.", { buffergap: realBufferGap, enterStarvation: localConf.starvationGap, }); this._inStarvationMode = true; } else if (this._inStarvationMode && realBufferGap >= localConf.outOfStarvationGap) { log_1.default.info("ABR", "exit starvation mode.", { bufferGap: realBufferGap, outOfStarvation: localConf.starvationGap, }); this._inStarvationMode = false; } } else if (this._inStarvationMode) { log_1.default.info("ABR", "exit starvation mode.", { bufferGap: realBufferGap, }); this._inStarvationMode = false; } // If in starvation mode, check if a quick new estimate can be done // from the last requests. // If so, cancel previous estimates and replace it by the new one if (this._inStarvationMode) { bandwidthEstimate = estimateStarvationModeBitrate(currentRequests, playbackInfo, currentRepresentation, this._lowLatencyMode, lastEstimatedBitrate); if (bandwidthEstimate !== undefined) { log_1.default.info("ABR", "starvation mode emergency estimate:", { bandwidth: bandwidthEstimate, }); bandwidthEstimator.reset(); newBitrateCeil = (0, is_null_or_undefined_1.default)(currentRepresentation) ? bandwidthEstimate : Math.min(bandwidthEstimate, currentRepresentation.bitrate); } } // if newBitrateCeil is not yet defined, do the normal estimation if ((0, is_null_or_undefined_1.default)(newBitrateCeil)) { bandwidthEstimate = bandwidthEstimator.getEstimate(); if (bandwidthEstimate !== undefined) { newBitrateCeil = bandwidthEstimate * (this._inStarvationMode ? localConf.starvationBitrateFactor : localConf.regularBitrateFactor); } else if (lastEstimatedBitrate !== undefined) { newBitrateCeil = lastEstimatedBitrate * (this._inStarvationMode ? localConf.starvationBitrateFactor : localConf.regularBitrateFactor); } else { newBitrateCeil = this._initialBitrate; } } if (playbackInfo.speed > 1) { newBitrateCeil /= playbackInfo.speed; } return { bandwidthEstimate: bandwidthEstimate, bitrateChosen: newBitrateCeil }; }; /** * For a given wanted bitrate, tells if should switch urgently. * @param {number} bitrate - The new estimated bitrate. * @param {Object|null} currentRepresentation - The Representation being * presently being loaded. * @param {Array.<Object>} currentRequests - All segment requests by segment's * start chronological order * @param {Object} playbackInfo - Information on the current playback. * @returns {boolean} */ NetworkAnalyzer.prototype.isUrgent = function (bitrate, currentRepresentation, currentRequests, playbackInfo) { if (currentRepresentation === null) { return true; } else if (bitrate >= currentRepresentation.bitrate) { return false; } return shouldDirectlySwitchToLowBitrate(playbackInfo, currentRequests, this._lowLatencyMode); }; return NetworkAnalyzer; }()); exports.default = NetworkAnalyzer;