Source: media/simple_abr_manager.js

/**
 * @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;
};