/**
* @license
* Copyright 2016 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.abr.SimpleAbrManager');
goog.require('goog.asserts');
goog.require('shaka.abr.EwmaBandwidthEstimator');
goog.require('shaka.log');
/**
* Creates a new SimpleAbrManager.
*
* @constructor
* @struct
* @implements {shakaExtern.AbrManager}
* @export
*/
shaka.abr.SimpleAbrManager = function() {
/** @private {?shakaExtern.AbrManager.SwitchCallback} */
this.switch_ = null;
/** @private {boolean} */
this.enabled_ = false;
/** @private {shaka.abr.EwmaBandwidthEstimator} */
this.bandwidthEstimator_ = new shaka.abr.EwmaBandwidthEstimator();
/**
* The last StreamSets given to us via chooseStreams().
* @private {Object.<string, shakaExtern.StreamSet>}
*/
this.streamSetsByType_ = {};
/**
* The last Streams chosen.
* @private {Object.<string, shakaExtern.Stream>}
*/
this.streamsByType_ = {};
/** @private {boolean} */
this.startupComplete_ = false;
/**
* The last wall-clock time, in milliseconds, when Streams were chosen via
* chooseStreams() or switch_().
*
* @private {?number}
*/
this.lastTimeChosenMs_ = null;
};
/**
* 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.
*
* @const {number}
*/
shaka.abr.SimpleAbrManager.STARTUP_INTERVAL_MS = 4000;
/**
* The minimum amount of time that must pass between switches, in milliseconds.
* This keeps us from changing too often and annoying the user.
*
* @const {number}
*/
shaka.abr.SimpleAbrManager.SWITCH_INTERVAL_MS = 8000;
/**
* The fraction of the estimated bandwidth which we should try to use when
* upgrading.
*
* @private
* @const {number}
*/
shaka.abr.SimpleAbrManager.BANDWIDTH_UPGRADE_TARGET_ = 0.85;
/**
* The largest fraction of the estimated bandwidth we should use. We should
* downgrade to avoid this.
*
* @private
* @const {number}
*/
shaka.abr.SimpleAbrManager.BANDWIDTH_DOWNGRADE_TARGET_ = 0.95;
/**
* The number of seconds of content to leave in buffer ahead of the playhead
* when upgrading video. This makes video upgrades visible sooner.
*
* @private
* @const {number}
*/
shaka.abr.SimpleAbrManager.UPGRADE_LEAVE_IN_BUFFER_ = 10;
/** @override */
shaka.abr.SimpleAbrManager.prototype.stop = function() {
this.switch_ = null;
this.enabled_ = false;
this.streamSetsByType_ = {};
this.streamsByType_ = {};
this.lastTimeChosenMs_ = null;
// Don't reset |startupComplete_|: if we've left the startup interval then we
// can start using bandwidth estimates right away if init() is called again.
};
/** @override */
shaka.abr.SimpleAbrManager.prototype.init = function(switchCallback) {
this.switch_ = switchCallback;
};
/** @override */
shaka.abr.SimpleAbrManager.prototype.chooseStreams = function(
streamSetsByType) {
// Merge StreamSets. We may have been given a partial list.
for (var type in streamSetsByType) {
this.streamSetsByType_[type] = streamSetsByType[type];
}
// Choose streams for the specific types requested.
var chosen = {};
if ('audio' in streamSetsByType) {
var audioStream = this.chooseAudioStream_();
if (audioStream) {
chosen['audio'] = audioStream;
this.streamsByType_['audio'] = audioStream;
} else {
delete this.streamsByType_['audio'];
}
}
if ('video' in streamSetsByType) {
var videoStream = this.chooseVideoStream_();
if (videoStream) {
chosen['video'] = videoStream;
this.streamsByType_['video'] = videoStream;
} else {
delete this.streamsByType_['video'];
}
}
if ('text' in streamSetsByType) {
// We don't adapt text, so just choose stream 0.
chosen['text'] = streamSetsByType['text'].streams[0];
}
this.lastTimeChosenMs_ = Date.now();
return chosen;
};
/** @override */
shaka.abr.SimpleAbrManager.prototype.enable = function() {
this.enabled_ = true;
};
/** @override */
shaka.abr.SimpleAbrManager.prototype.disable = function() {
this.enabled_ = false;
};
/** @override */
shaka.abr.SimpleAbrManager.prototype.segmentDownloaded = function(
startTimeMs, endTimeMs, numBytes) {
shaka.log.v2('Segment downloaded:',
'startTimeMs=' + startTimeMs,
'endTimeMs=' + endTimeMs,
'numBytes=' + numBytes);
goog.asserts.assert(endTimeMs >= startTimeMs,
'expected a non-negative duration');
this.bandwidthEstimator_.sample(endTimeMs - startTimeMs, numBytes);
if ((this.lastTimeChosenMs_ != null) && this.enabled_)
this.suggestStreams_();
};
/** @override */
shaka.abr.SimpleAbrManager.prototype.getBandwidthEstimate = function() {
return this.bandwidthEstimator_.getBandwidthEstimate();
};
/** @override */
shaka.abr.SimpleAbrManager.prototype.setDefaultEstimate = function(estimate) {
this.bandwidthEstimator_.setDefaultEstimate(estimate);
};
/**
* Calls switch_() with which Streams to switch to.
*
* @private
*/
shaka.abr.SimpleAbrManager.prototype.suggestStreams_ = function() {
shaka.log.v2('Suggesting Streams...');
goog.asserts.assert(this.lastTimeChosenMs_ != null,
'lastTimeChosenMs_ should not be null');
var now = Date.now();
var delta = now - this.lastTimeChosenMs_;
// Check if we've left the startup interval.
if (!this.startupComplete_) {
if (delta < shaka.abr.SimpleAbrManager.STARTUP_INTERVAL_MS) {
shaka.log.v2('Still within startup interval...');
return;
}
this.startupComplete_ = true;
} else {
// Check if we've left the switch interval.
if (delta < shaka.abr.SimpleAbrManager.SWITCH_INTERVAL_MS) {
shaka.log.v2('Still within switch interval...');
return;
}
}
var oldVideo = this.streamsByType_['video'];
var chosen = this.chooseStreams_();
// Do not clear the buffer.
var opt_leaveInBuffer = undefined;
if (oldVideo && chosen.video &&
chosen.video.bandwidth > oldVideo.bandwidth) {
// We're upgrading video.
// Leave some in buffer, but clear ahead of that.
opt_leaveInBuffer = shaka.abr.SimpleAbrManager.UPGRADE_LEAVE_IN_BUFFER_;
}
var currentBandwidthKbps =
Math.round(this.bandwidthEstimator_.getBandwidthEstimate() / 1000.0);
shaka.log.debug(
'Calling switch_()...',
'bandwidth=' + currentBandwidthKbps + ' kbps',
'opt_leaveInBuffer=', opt_leaveInBuffer);
// If any of these chosen streams are already chosen, Player will filter them
// out before passing the choices on to StreamingEngine.
this.switch_(chosen, opt_leaveInBuffer);
};
/**
* Chooses which Streams to switch to.
*
* @return {!Object.<string, !shakaExtern.Stream>}
* @private
*/
shaka.abr.SimpleAbrManager.prototype.chooseStreams_ = function() {
var streamsByType = {};
// Choose audio Stream.
var audioStream = this.chooseAudioStream_();
if (audioStream) {
streamsByType['audio'] = audioStream;
this.streamsByType_['audio'] = audioStream;
}
// Choose video Stream.
var videoStream = this.chooseVideoStream_();
if (videoStream) {
streamsByType['video'] = videoStream;
this.streamsByType_['video'] = videoStream;
}
this.lastTimeChosenMs_ = Date.now();
return streamsByType;
};
/**
* Chooses which audio Stream to switch to.
*
* @return {?shakaExtern.Stream}
* @private
*/
shaka.abr.SimpleAbrManager.prototype.chooseAudioStream_ = function() {
// Alias.
var SimpleAbrManager = shaka.abr.SimpleAbrManager;
// Get sorted audio Streams.
var audioStreamSet = this.streamSetsByType_['audio'];
if (!audioStreamSet)
return null;
var audioStreams = SimpleAbrManager.sortStreamsByBandwidth_(audioStreamSet);
// Just pick the middle one.
// TODO: Implement better audio adaptation.
return audioStreams[Math.floor(audioStreams.length / 2)];
};
/**
* Chooses which video Stream to switch to.
*
* @return {?shakaExtern.Stream}
* @private
*/
shaka.abr.SimpleAbrManager.prototype.chooseVideoStream_ = function() {
// Alias.
var SimpleAbrManager = shaka.abr.SimpleAbrManager;
// Get sorted video Streams.
var videoStreamSet = this.streamSetsByType_['video'];
if (!videoStreamSet)
return null;
var videoStreams = SimpleAbrManager.sortStreamsByBandwidth_(videoStreamSet);
var audioStream = this.streamsByType_['audio'];
var audioBandwidth = (audioStream && audioStream.bandwidth) || 0;
var currentBandwidth = this.bandwidthEstimator_.getBandwidthEstimate();
// Start by assuming that we will use the first Stream.
var chosen = videoStreams[0];
for (var i = 0; i < videoStreams.length; ++i) {
var stream = videoStreams[i];
var nextStream = (i + 1 < videoStreams.length) ?
videoStreams[i + 1] :
{bandwidth: Number.POSITIVE_INFINITY};
// Ignore Streams which don't have bandwidth information.
if (!stream.bandwidth) continue;
var minBandwidth = (stream.bandwidth + audioBandwidth) /
SimpleAbrManager.BANDWIDTH_DOWNGRADE_TARGET_;
var maxBandwidth = (nextStream.bandwidth + audioBandwidth) /
SimpleAbrManager.BANDWIDTH_UPGRADE_TARGET_;
shaka.log.v2('Bandwidth ranges:',
((stream.bandwidth + audioBandwidth) / 1e6).toFixed(3),
(minBandwidth / 1e6).toFixed(3),
(maxBandwidth / 1e6).toFixed(3));
if (currentBandwidth >= minBandwidth && currentBandwidth <= maxBandwidth)
chosen = stream;
}
return chosen;
};
/**
* @param {!shakaExtern.StreamSet} streamSet
* @return {!Array.<shakaExtern.Stream>} |streamSet|'s Streams sorted
* in ascending order of bandwidth.
* @private
*/
shaka.abr.SimpleAbrManager.sortStreamsByBandwidth_ = function(streamSet) {
return streamSet.streams.slice(0)
.filter(function(s) {
return s.allowedByApplication && s.allowedByKeySystem;
})
.sort(function(s1, s2) { return s1.bandwidth - s2.bandwidth; });
};