/**
* @license
* Copyright 2015 Google Inc.
*
* 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.
*/
goog.provide('shaka.media.SimpleAbrManager');
goog.require('shaka.asserts');
goog.require('shaka.log');
goog.require('shaka.media.IAbrManager');
goog.require('shaka.player.AudioTrack');
goog.require('shaka.player.IVideoSource');
goog.require('shaka.player.VideoTrack');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.IBandwidthEstimator');
/**
* Creates a SimpleAbrManager, which selects video tracks using a basic set of
* heuristics.
*
* @struct
* @constructor
* @implements {shaka.media.IAbrManager}
* @export
*/
shaka.media.SimpleAbrManager = function() {
/** @private {shaka.util.IBandwidthEstimator} */
this.estimator_ = null;
/** @private {shaka.player.IVideoSource} */
this.videoSource_ = null;
/** @private {!shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
/**
* The timestamp after which we are allowed to adapt, in milliseconds.
* @private {number}
*/
this.nextAdaptationTime_ = Number.POSITIVE_INFINITY;
/** @private {boolean} */
this.enabled_ = true;
/** @private {boolean} */
this.started_ = false;
};
/**
* The minimum amount of time that must pass before the first switch, in
* milliseconds. This gives the bandwidth estimator time to get some real
* data before changing anything.
*
* @private
* @const {number}
*/
shaka.media.SimpleAbrManager.FIRST_SWITCH_INTERVAL_ = 4000;
/**
* The minimum amount of time that must pass between switches, in milliseconds.
* This keeps us from changing too often and annoying the user.
*
* @private
* @const {number}
*/
shaka.media.SimpleAbrManager.MIN_SWITCH_INTERVAL_ = 30000;
/**
* The minimum amount of time that must pass between bandwidth evaluations, in
* milliseconds. This keeps us from checking for adaptation opportunities too
* often.
*
* @private
* @const {number}
*/
shaka.media.SimpleAbrManager.MIN_EVAL_INTERVAL_ = 3000;
/**
* The fraction of the estimated bandwidth which we should try to use when
* upgrading.
*
* @private
* @const {number}
*/
shaka.media.SimpleAbrManager.BANDWIDTH_UPGRADE_TARGET_ = 0.85;
/**
* The fraction of the estimated bandwidth we should downgrade to avoid
* exceeding.
*
* @private
* @const {number}
*/
shaka.media.SimpleAbrManager.BANDWIDTH_DOWNGRADE_TARGET_ = 0.95;
/**
* @override
* @suppress {checkTypes} to set otherwise non-nullable types to null.
*/
shaka.media.SimpleAbrManager.prototype.destroy = function() {
this.eventManager_.destroy();
this.eventManager_ = null;
this.estimator_ = null;
this.videoSource_ = null;
};
/** @override */
shaka.media.SimpleAbrManager.prototype.initialize = function(
estimator, videoSource) {
if (this.estimator_ || this.videoSource_) {
return;
}
this.estimator_ = estimator;
this.videoSource_ = videoSource;
};
/** @override */
shaka.media.SimpleAbrManager.prototype.start = function() {
if (!this.estimator_ || !this.videoSource_ || this.started_) {
return;
}
this.nextAdaptationTime_ =
Date.now() + shaka.media.SimpleAbrManager.FIRST_SWITCH_INTERVAL_;
this.eventManager_.listen(this.estimator_, 'bandwidth',
this.onBandwidth_.bind(this));
this.eventManager_.listen(this.videoSource_, 'adaptation',
this.onAdaptation_.bind(this));
this.eventManager_.listen(this.videoSource_, 'trackschanged',
this.chooseNewTrack_.bind(this));
this.started_ = true;
};
/** @override */
shaka.media.SimpleAbrManager.prototype.enable = function(enabled) {
this.enabled_ = enabled;
};
/** @override */
shaka.media.SimpleAbrManager.prototype.getInitialVideoTrackId = function() {
if (!this.estimator_ || !this.videoSource_) {
return null;
}
var chosen = this.chooseVideoTrack_();
return chosen ? chosen.id : null;
};
/**
* Select the specified video track.
*
* Does nothing if the AbrManager has not been started.
*
* @param {shaka.player.VideoTrack} track the track to the switch to
* @param {boolean} clearBuffer If true, removes the previous stream's content
* before switching to the new stream.
* @param {number=} opt_clearBufferOffset if |clearBuffer| and
* |opt_clearBufferOffset| are truthy, clear the stream buffer from the
* given offset (relative to the video's current time) to the end of the
* stream.
*
* @protected
* @expose
*/
shaka.media.SimpleAbrManager.prototype.selectVideoTrack = function(
track, clearBuffer, opt_clearBufferOffset) {
if (!this.started_) {
return;
}
this.videoSource_.selectVideoTrack(
track.id, clearBuffer, opt_clearBufferOffset);
};
/**
* Find the active track in the list.
*
* @param {!Array.<T>} trackList
* @return {T}
*
* @template T
* @private
*/
shaka.media.SimpleAbrManager.findActiveTrack_ = function(trackList) {
for (var i = 0; i < trackList.length; ++i) {
if (trackList[i].active) {
return trackList[i];
}
}
return null;
};
/**
* Handles bandwidth update events.
*
* @param {!Event} event
* @private
*/
shaka.media.SimpleAbrManager.prototype.onBandwidth_ = function(event) {
if (Date.now() < this.nextAdaptationTime_) {
return;
}
this.chooseNewTrack_(event);
};
/**
* Makes adaptation decisions.
*
* @param {!Event} event
* @private
*/
shaka.media.SimpleAbrManager.prototype.chooseNewTrack_ = function(event) {
if (!this.enabled_) {
return;
}
// Alias.
var SimpleAbrManager = shaka.media.SimpleAbrManager;
var chosen = this.chooseVideoTrack_();
if (chosen) {
if (chosen.active) {
// We are already using the correct video track.
this.nextAdaptationTime_ =
Date.now() + SimpleAbrManager.MIN_EVAL_INTERVAL_;
return;
}
shaka.log.info('Video adaptation:', chosen);
this.selectVideoTrack(chosen, false);
}
// Can't adapt again until we get confirmation of this one.
this.nextAdaptationTime_ = Number.POSITIVE_INFINITY;
};
/**
* Handles adaptation events.
*
* @param {!Event} event
* @private
*/
shaka.media.SimpleAbrManager.prototype.onAdaptation_ = function(event) {
// This check allows us to ignore the initial adaptation events, which would
// otherwise cause us not to honor FIRST_SWITCH_INTERVAL_.
if (this.nextAdaptationTime_ == Number.POSITIVE_INFINITY) {
// Adaptation is complete, so schedule the next adaptation.
this.nextAdaptationTime_ =
Date.now() + shaka.media.SimpleAbrManager.MIN_SWITCH_INTERVAL_;
}
};
/**
* Choose a video track based on current bandwidth conditions.
*
* @return {shaka.player.VideoTrack} The chosen video track or null if there
* are no video tracks to choose.
* @private
*/
shaka.media.SimpleAbrManager.prototype.chooseVideoTrack_ = function() {
// Alias.
var SimpleAbrManager = shaka.media.SimpleAbrManager;
var videoTracks = this.videoSource_.getVideoTracks();
if (videoTracks.length == 0) {
return null;
}
videoTracks.sort(shaka.player.VideoTrack.compare);
var activeAudioTrack =
SimpleAbrManager.findActiveTrack_(this.videoSource_.getAudioTracks());
var audioBandwidth = activeAudioTrack ? activeAudioTrack.bandwidth : 0;
var bandwidth = this.estimator_.getBandwidth();
// Start by assuming that we will use the first track.
var chosen = videoTracks[0];
for (var i = 0; i < videoTracks.length; ++i) {
var track = videoTracks[i];
var nextTrack = (i + 1 < videoTracks.length) ?
videoTracks[i + 1] :
{ bandwidth: Number.POSITIVE_INFINITY };
// Ignore any track which is missing bandwidth info.
if (!track.bandwidth) continue;
var minBandwidth = (track.bandwidth + audioBandwidth) /
SimpleAbrManager.BANDWIDTH_DOWNGRADE_TARGET_;
var maxBandwidth = (nextTrack.bandwidth + audioBandwidth) /
SimpleAbrManager.BANDWIDTH_UPGRADE_TARGET_;
shaka.log.v2('Bandwidth ranges:',
((track.bandwidth + audioBandwidth) / 1e6).toFixed(3),
(minBandwidth / 1e6).toFixed(3),
(maxBandwidth / 1e6).toFixed(3));
if (bandwidth >= minBandwidth && bandwidth <= maxBandwidth) {
chosen = track;
if (chosen.active) break;
}
}
return chosen;
};