Source: lib/media/live_catchup_controller.js

goog.provide('shaka.media.LiveCatchUpController');

goog.require('shaka.util.Timer');


/**
 * The live catch up controller that tries to minimize latency to live edge.
 */
shaka.media.LiveCatchUpController = class {
  /**
   * @param {shaka.media.LiveCatchUpController.PlayerInterface} playerInterface
   */
  constructor(playerInterface) {
    /** @private {?shaka.media.LiveCatchUpController.PlayerInterface} */
    this.playerInterface_ = playerInterface;

    /** @private {?shaka.extern.LiveCatchUpConfiguration} */
    this.config_ = null;

    /** @private {HTMLMediaElement} */
    this.video_ = null;

    /** @private {boolean} */
    this.enabled_ = false;

    /** @private {number} */
    this.updateInterval_ = 0.5;

    /** @private {number} */
    this.defaultMaxPlayRate_ = 1.1;

    /** @private {number} */
    this.defaultMinPlayRate_ = 0.9;

    /** @private {number} */
    this.lastPlayRate_ = 1.0;

    /** @private {shaka.util.Timer} */
    this.updateTimer_ = new shaka.util.Timer(() => {
      this.update_();
    });
  }

  /**
   * Called by the Player to provide an updated configuration any time it
   * changes. Must be called at least once before start().
   *
   * @param {shaka.extern.LiveCatchUpConfiguration} config
   */
  configure(config) {
    this.config_ = config;
  }

  /**
   * Attach to the video element.
   * @param {HTMLMediaElement} video
   */
  setVideoElement(video) {
    this.video_ = video;
  }

  /**
   *
   */
  enable() {
    this.enabled_ = true;
    this.updateTimer_.tickAfter(this.updateInterval_);
  }

  /**
   *
   */
  disable() {
    this.enabled_ = false;
    this.updateTimer_.stop();
  }

  /**
   * @private
   */
  update_() {
    if (this.video_ && !this.video_.paused) {
      this.updatePlayRate();
    }
    if (this.enabled_) {
      this.updateTimer_.tickAfter(this.updateInterval_);
    }
  }

  /**
   *
   */
  updatePlayRate() {
    const currentPlayRate = this.playerInterface_.getPlayRate();
    if (currentPlayRate <= 0) {
      return;
    }

    const newPlayRate = this.calculateNewPlaybackRate_();
    if (newPlayRate != currentPlayRate) {
      this.playerInterface_.trickPlay(newPlayRate);
    }
  }

  /**
   * @return {number}
   * @private
   */
  calculateNewPlaybackRate_() {
    const maxRate = this.getPlaybackRateMax_();
    const minRate = this.getPlaybackRateMin_();
    const maxBuffer = this.config_.maxBuffer;
    const minBuffer = this.config_.minBuffer;

    const delay = this.playerInterface_.getBufferEnd() -
        this.playerInterface_.getPresentationTime();

    // Assume maxRate >= 1 and minRate <= 1
    let newRate = 1.0;
    if (delay >= maxBuffer) {
      // Blend between 1.0 ~ maxRate
      newRate = 1.0 + (maxRate - 1.0) * (delay - maxBuffer) / maxBuffer;
    } else if (delay <= minBuffer) {
      // Blend between minRate ~ 1.0
      newRate = 1.0 - (1.0 - minRate) * (minBuffer - delay) / minBuffer;
    }

    if (newRate > maxRate) {
      newRate = maxRate;
    }

    if (newRate < minRate) {
      newRate = minRate;
    }

    const blend = this.config_.playbackRateBlend;
    newRate = blend * this.lastPlayRate_ + (1.0 - blend) * newRate;
    this.lastPlayRate_ = newRate;
    return newRate;
  }

  /**
   * @return {number}
   */
  getDefaultMaxPlayRate() {
    return this.defaultMaxPlayRate_;
  }

  /**
   * @return {number}
   * @private
   */
  getPlaybackRateMax_() {
    let maxRate = this.defaultMaxPlayRate_;
    const serviceDescription = this.playerInterface_.getServiceDescription();
    if (serviceDescription && serviceDescription.playbackRate) {
      maxRate = serviceDescription.playbackRate.max;
    }
    if (this.config_.playbackRateMaxOverride > 0) {
      maxRate = this.config_.playbackRateMaxOverride;
    }
    return maxRate;
  }

  /**
   * @return {number}
   * @private
   */
  getPlaybackRateMin_() {
    let minRate = this.defaultMinPlayRate_;
    const serviceDescription = this.playerInterface_.getServiceDescription();
    if (serviceDescription && serviceDescription.playbackRate) {
      minRate = serviceDescription.playbackRate.min;
    }
    if (this.config_.playbackRateMaxOverride > 0) {
      minRate = this.config_.playbackRateMinOverride;
    }
    return minRate;
  }
};

/**
 * @typedef {{
 *   getBufferEnd: function():number,
 *   getPlayRate: function():number,
 *   getPresentationTime: function():number,
 *   getPresentationLatency: function():number,
 *   trickPlay: function(number),
 *   getServiceDescription:
 *   (function():shaka.extern.ServiceDescription|undefined)
 * }}
 *
 * @property {function():number} getBufferEnd
 *   Get the Buffer end.
 * @property {function():number} getPlayRate
 *   Get the current play rate.
 * @property {function():number} getPresentationTime
 *   Get the position in the presentation (in seconds) of the content that the
 *   viewer is seeing on screen right now.
 * @property {function():number} getPresentationLatency
 *   Get the presentation latency
 * @property {function(number)} trickPlay
 *   Called when an event occurs that should be sent to the app.
 */
shaka.media.LiveCatchUpController.PlayerInterface;