rx-player
Version:
Canal+ HTML5 Video Player
412 lines (411 loc) • 21.1 kB
JavaScript
"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.default = createAdaptiveRepresentationSelector;
var config_1 = require("../../config");
var log_1 = require("../../log");
var is_null_or_undefined_1 = require("../../utils/is_null_or_undefined");
var noop_1 = require("../../utils/noop");
var ranges_1 = require("../../utils/ranges");
var reference_1 = require("../../utils/reference");
var task_canceller_1 = require("../../utils/task_canceller");
var buffer_based_chooser_1 = require("./buffer_based_chooser");
var guess_based_chooser_1 = require("./guess_based_chooser");
var network_analyzer_1 = require("./network_analyzer");
var bandwidth_estimator_1 = require("./utils/bandwidth_estimator");
var filter_by_bitrate_1 = require("./utils/filter_by_bitrate");
var filter_by_resolution_1 = require("./utils/filter_by_resolution");
var last_estimate_storage_1 = require("./utils/last_estimate_storage");
var pending_requests_store_1 = require("./utils/pending_requests_store");
var representation_score_calculator_1 = require("./utils/representation_score_calculator");
var select_optimal_representation_1 = require("./utils/select_optimal_representation");
// Create default shared references
var limitResolutionDefaultRef = new reference_1.default(undefined);
limitResolutionDefaultRef.finish();
var throttleBitrateDefaultRef = new reference_1.default(Infinity);
throttleBitrateDefaultRef.finish();
/**
* Select the most adapted Representation according to the network and buffer
* metrics it receives.
*
* @param {Object} options - Initial configuration (see type definition)
* @returns {Object} - Interface allowing to select a Representation.
* @see IRepresentationEstimator
*/
function createAdaptiveRepresentationSelector(options) {
/**
* Allows to estimate the current network bandwidth.
* One per active media type.
*/
var bandwidthEstimators = {};
var initialBitrates = options.initialBitrates, throttlers = options.throttlers, lowLatencyMode = options.lowLatencyMode;
/**
* Returns Object emitting Representation estimates as well as callbacks
* allowing to helping it produce them.
*
* @see IRepresentationEstimator
* @param {Object} context
* @param {Object} currentRepresentation
* @param {Object} representations
* @param {Object} playbackObserver
* @param {Object} stopAllEstimates
* @returns {Array.<Object>}
*/
return function getEstimates(context, currentRepresentation, representations, playbackObserver, stopAllEstimates) {
var _a, _b, _c;
var type = context.adaptation.type;
var bandwidthEstimator = _getBandwidthEstimator(type);
var initialBitrate = (_a = initialBitrates[type]) !== null && _a !== void 0 ? _a : 0;
var filters = {
limitResolution: (_b = throttlers.limitResolution[type]) !== null && _b !== void 0 ? _b : limitResolutionDefaultRef,
throttleBitrate: (_c = throttlers.throttleBitrate[type]) !== null && _c !== void 0 ? _c : throttleBitrateDefaultRef,
};
return getEstimateReference({
bandwidthEstimator: bandwidthEstimator,
context: context,
currentRepresentation: currentRepresentation,
filters: filters,
initialBitrate: initialBitrate,
playbackObserver: playbackObserver,
representations: representations,
lowLatencyMode: lowLatencyMode,
}, stopAllEstimates);
};
/**
* Returns interface allowing to estimate network throughtput for a given type.
* @param {string} bufferType
* @returns {Object}
*/
function _getBandwidthEstimator(bufferType) {
var originalBandwidthEstimator = bandwidthEstimators[bufferType];
if ((0, is_null_or_undefined_1.default)(originalBandwidthEstimator)) {
log_1.default.debug("ABR", "Creating new BandwidthEstimator", { bufferType: bufferType });
var bandwidthEstimator = new bandwidth_estimator_1.default();
bandwidthEstimators[bufferType] = bandwidthEstimator;
return bandwidthEstimator;
}
return originalBandwidthEstimator;
}
}
/**
* Estimate regularly the current network bandwidth and the best Representation
* that can be played according to the current network and playback conditions.
*
* `getEstimateReference` only does estimations for a given type (e.g.
* "audio", "video" etc.) and Period.
*
* If estimates for multiple types and/or Periods are needed, you should
* call `getEstimateReference` as many times.
*
* This function returns a tuple:
* - the first element being the object through which estimates will be produced
* - the second element being callbacks that have to be triggered at various
* events to help it doing those estimates.
*
* @param {Object} args
* @param {Object} stopAllEstimates
* @returns {Array.<Object>}
*/
function getEstimateReference(_a, stopAllEstimates) {
var bandwidthEstimator = _a.bandwidthEstimator, context = _a.context, currentRepresentation = _a.currentRepresentation, filters = _a.filters, initialBitrate = _a.initialBitrate, lowLatencyMode = _a.lowLatencyMode, playbackObserver = _a.playbackObserver, representationsRef = _a.representations;
var scoreCalculator = new representation_score_calculator_1.default();
var networkAnalyzer = new network_analyzer_1.default(initialBitrate !== null && initialBitrate !== void 0 ? initialBitrate : 0, lowLatencyMode);
var requestsStore = new pending_requests_store_1.default();
/**
* Callback called each time a new segment is pushed, with the information on the
* new pushed segment.
*/
var onAddedSegment = noop_1.default;
var callbacks = {
metrics: onMetric,
requestBegin: onRequestBegin,
requestProgress: onRequestProgress,
requestEnd: onRequestEnd,
addedSegment: function (val) {
onAddedSegment(val);
},
};
/**
* `TaskCanceller` allowing to stop producing estimate.
* This TaskCanceller is used both for restarting estimates with a new
* configuration and to cancel them altogether.
*/
var currentEstimatesCanceller = new task_canceller_1.default();
currentEstimatesCanceller.linkToSignal(stopAllEstimates);
// Create `SharedReference` on which estimates will be emitted.
var estimateRef = createEstimateReference(representationsRef.getValue(), currentEstimatesCanceller.signal);
representationsRef.onUpdate(restartEstimatesProductionFromCurrentConditions, {
clearSignal: stopAllEstimates,
});
return { estimates: estimateRef, callbacks: callbacks };
/**
* Emit the ABR estimates on various events through the returned
* `SharedReference`.
* @param {Array.<Object>} unsortedRepresentations - All `Representation` that
* the ABR logic can choose from.
* @param {Object} innerCancellationSignal - When this `CancellationSignal`
* emits, all events registered to to emit new estimates will be unregistered.
* Note that the returned reference may still be used to produce new estimates
* in the future if you want to even after this signal emits.
* @returns {Object} - `SharedReference` through which ABR estimates will be
* produced.
*/
function createEstimateReference(unsortedRepresentations, innerCancellationSignal) {
if (unsortedRepresentations.length <= 1) {
// There's only a single Representation. Just choose it.
return new reference_1.default({
bitrate: undefined,
representation: unsortedRepresentations[0],
urgent: true,
knownStableBitrate: undefined,
});
}
/** If true, Representation estimates based on the buffer health might be used. */
var allowBufferBasedEstimates = false;
/** Ensure `Representation` objects are sorted by bitrates and only rely on this. */
var sortedRepresentations = unsortedRepresentations.sort(function (ra, rb) { return ra.bitrate - rb.bitrate; });
/**
* Module calculating the optimal Representation based on the current
* buffer's health (i.e. whether enough data is buffered, history of
* buffer size etc.).
*/
var bufferBasedChooser = new buffer_based_chooser_1.default(sortedRepresentations.map(function (r) { return r.bitrate; }));
/** Store the previous estimate made here. */
var prevEstimate = new last_estimate_storage_1.default();
/**
* Module calculating the optimal Representation by "guessing it" with a
* step-by-step algorithm.
* Only used in very specific scenarios.
*/
var guessBasedChooser = new guess_based_chooser_1.default(scoreCalculator, prevEstimate);
// get initial observation for initial estimate
var lastPlaybackObservation = playbackObserver.getReference().getValue();
/** Reference through which estimates are emitted. */
var innerEstimateRef = new reference_1.default(getCurrentEstimate());
// Listen to playback observations
playbackObserver.listen(function (obs) {
lastPlaybackObservation = obs;
updateEstimate();
}, { includeLastObservation: false, clearSignal: innerCancellationSignal });
onAddedSegment = function (val) {
if (lastPlaybackObservation === null) {
return;
}
var position = lastPlaybackObservation.position, speed = lastPlaybackObservation.speed;
var timeRanges = val.buffered;
var bufferGap = (0, ranges_1.getLeftSizeOfRange)(timeRanges, position.getWanted());
var representation = val.content.representation;
var currentScore = scoreCalculator.getEstimate(representation);
var currentBitrate = representation.bitrate;
var observation = { bufferGap: bufferGap, currentBitrate: currentBitrate, currentScore: currentScore, speed: speed };
bufferBasedChooser.onAddedSegment(observation);
updateEstimate();
};
innerCancellationSignal.register(function () {
onAddedSegment = noop_1.default;
});
filters.throttleBitrate.onUpdate(updateEstimate, {
clearSignal: innerCancellationSignal,
});
filters.limitResolution.onUpdate(updateEstimate, {
clearSignal: innerCancellationSignal,
});
return innerEstimateRef;
function updateEstimate() {
innerEstimateRef.setValue(getCurrentEstimate());
}
/** Returns the actual estimate based on all methods and algorithm available. */
function getCurrentEstimate() {
var bufferGap = lastPlaybackObservation.bufferGap, position = lastPlaybackObservation.position, maximumPosition = lastPlaybackObservation.maximumPosition;
var resolutionLimit = filters.limitResolution.getValue();
var bitrateThrottle = filters.throttleBitrate.getValue();
var currentRepresentationVal = currentRepresentation.getValue();
var filteredReps = getFilteredRepresentations(sortedRepresentations, resolutionLimit, bitrateThrottle);
var requests = requestsStore.getRequests();
var _a = networkAnalyzer.getBandwidthEstimate(lastPlaybackObservation, bandwidthEstimator, currentRepresentationVal, requests, prevEstimate.bandwidth), bandwidthEstimate = _a.bandwidthEstimate, bitrateChosen = _a.bitrateChosen;
var stableRepresentation = scoreCalculator.getLastStableRepresentation();
var knownStableBitrate = stableRepresentation === null
? undefined
: stableRepresentation.bitrate /
(lastPlaybackObservation.speed > 0 ? lastPlaybackObservation.speed : 1);
var _b = config_1.default.getCurrent(), ABR_ENTER_BUFFER_BASED_ALGO = _b.ABR_ENTER_BUFFER_BASED_ALGO, ABR_EXIT_BUFFER_BASED_ALGO = _b.ABR_EXIT_BUFFER_BASED_ALGO;
if (allowBufferBasedEstimates && bufferGap <= ABR_EXIT_BUFFER_BASED_ALGO) {
allowBufferBasedEstimates = false;
}
else if (!allowBufferBasedEstimates &&
isFinite(bufferGap) &&
bufferGap >= ABR_ENTER_BUFFER_BASED_ALGO) {
allowBufferBasedEstimates = true;
}
/**
* Representation chosen when considering only [pessimist] bandwidth
* calculation.
* This is a safe enough choice but might be lower than what the user
* could actually profit from.
*/
var chosenRepFromBandwidth = (0, select_optimal_representation_1.default)(filteredReps, bitrateChosen);
/**
* Current optimal Representation's bandwidth choosen by a buffer-based
* adaptive algorithm.
*/
var currentBufferBasedEstimate = bufferBasedChooser.getLastEstimate();
var currentBestBitrate = chosenRepFromBandwidth.bitrate;
/**
* Representation chosen when considering the current buffer size.
* If defined, takes precedence over `chosenRepFromBandwidth`.
*
* This is a very safe choice, yet it is very slow and might not be
* adapted to cases where a buffer cannot be build, such as live contents.
*
* `null` if this buffer size mode is not enabled or if we don't have a
* choice from it yet.
*/
var chosenRepFromBufferSize = null;
if (allowBufferBasedEstimates &&
currentBufferBasedEstimate !== undefined &&
currentBufferBasedEstimate > currentBestBitrate) {
chosenRepFromBufferSize = (0, select_optimal_representation_1.default)(filteredReps, currentBufferBasedEstimate);
currentBestBitrate = chosenRepFromBufferSize.bitrate;
}
/**
* Representation chosen by the more adventurous `GuessBasedChooser`,
* which iterates through Representations one by one until finding one
* that cannot be "maintained".
*
* If defined, takes precedence over both `chosenRepFromBandwidth` and
* `chosenRepFromBufferSize`.
*
* This is the riskiest choice (in terms of rebuffering chances) but is
* only enabled when no other solution is adapted (for now, this just
* applies for low-latency contents when playing close to the live
* edge).
*
* `null` if not enabled or if there's currently no guess.
*/
var chosenRepFromGuessMode = null;
if (lowLatencyMode &&
currentRepresentationVal !== null &&
context.manifest.isDynamic &&
maximumPosition - position.getWanted() < 40) {
chosenRepFromGuessMode = guessBasedChooser.getGuess(sortedRepresentations, lastPlaybackObservation, currentRepresentationVal, currentBestBitrate, requests);
}
if (chosenRepFromGuessMode !== null &&
chosenRepFromGuessMode.bitrate > currentBestBitrate) {
log_1.default.debug("ABR", "new guess-based estimate", {
bitrate: chosenRepFromGuessMode.bitrate,
representation: chosenRepFromGuessMode.id,
});
prevEstimate.update(chosenRepFromGuessMode, bandwidthEstimate, 2 /* ABRAlgorithmType.GuessBased */);
return {
bitrate: bandwidthEstimate,
representation: chosenRepFromGuessMode,
urgent: currentRepresentationVal === null ||
chosenRepFromGuessMode.bitrate < currentRepresentationVal.bitrate,
knownStableBitrate: knownStableBitrate,
};
}
else if (chosenRepFromBufferSize !== null) {
log_1.default.debug("ABR", "new buffer-based estimate", {
bitrate: chosenRepFromBufferSize.bitrate,
representation: chosenRepFromBufferSize.id,
});
prevEstimate.update(chosenRepFromBufferSize, bandwidthEstimate, 0 /* ABRAlgorithmType.BufferBased */);
return {
bitrate: bandwidthEstimate,
representation: chosenRepFromBufferSize,
urgent: networkAnalyzer.isUrgent(chosenRepFromBufferSize.bitrate, currentRepresentationVal, requests, lastPlaybackObservation),
knownStableBitrate: knownStableBitrate,
};
}
else {
log_1.default.debug("ABR", "new bandwidth estimate", {
bitrate: chosenRepFromBandwidth.bitrate,
representation: chosenRepFromBandwidth.id,
});
prevEstimate.update(chosenRepFromBandwidth, bandwidthEstimate, 1 /* ABRAlgorithmType.BandwidthBased */);
return {
bitrate: bandwidthEstimate,
representation: chosenRepFromBandwidth,
urgent: networkAnalyzer.isUrgent(chosenRepFromBandwidth.bitrate, currentRepresentationVal, requests, lastPlaybackObservation),
knownStableBitrate: knownStableBitrate,
};
}
}
}
/**
* Stop previous estimate production (if one) and restart it considering new
* conditions (such as a new list of Representations).
*/
function restartEstimatesProductionFromCurrentConditions() {
var representations = representationsRef.getValue();
currentEstimatesCanceller.cancel();
currentEstimatesCanceller = new task_canceller_1.default();
currentEstimatesCanceller.linkToSignal(stopAllEstimates);
var newRef = createEstimateReference(representations, currentEstimatesCanceller.signal);
newRef.onUpdate(function onNewEstimate(newEstimate) {
estimateRef.setValue(newEstimate);
}, { clearSignal: currentEstimatesCanceller.signal, emitCurrentValue: true });
}
/**
* Callback to call when new metrics are available
* @param {Object} value
*/
function onMetric(value) {
var requestDuration = value.requestDuration, segmentDuration = value.segmentDuration, size = value.size, content = value.content;
// calculate bandwidth
bandwidthEstimator.addSample(requestDuration, size);
if (!content.segment.isInit) {
// calculate "maintainability score"
var segment = content.segment, representation = content.representation;
if (segmentDuration === undefined && !segment.complete) {
// We cannot know the real duration of the segment
return;
}
var segDur = segmentDuration !== null && segmentDuration !== void 0 ? segmentDuration : segment.duration;
scoreCalculator.addSample(representation, requestDuration / 1000, segDur);
}
}
/** Callback called when a new request begins. */
function onRequestBegin(val) {
requestsStore.add(val);
}
/** Callback called when progress information is known on a pending request. */
function onRequestProgress(val) {
requestsStore.addProgress(val);
}
/** Callback called when a pending request ends. */
function onRequestEnd(val) {
requestsStore.remove(val.id);
}
}
/**
* Filter representations given through filters options.
* @param {Array.<Representation>} representations
* @param {Object | undefined} resolutionLimit
* @param {number | undefined} bitrateThrottle
* @returns {Array.<Representation>}
*/
function getFilteredRepresentations(representations, resolutionLimit, bitrateThrottle) {
var filteredReps = representations;
if (bitrateThrottle !== undefined && bitrateThrottle < Infinity) {
filteredReps = (0, filter_by_bitrate_1.default)(filteredReps, bitrateThrottle);
}
if (resolutionLimit !== undefined) {
filteredReps = (0, filter_by_resolution_1.default)(filteredReps, resolutionLimit);
}
return filteredReps;
}