Source: lib/media/live_catchup_controller.js

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

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


/**
 * 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_ = 2.0;

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

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

    /**
     * The default maximum difference between the current live offset and the
     * target live offset, in milliseconds, for which unit speed (1.0f) is used.
     * @private {number}
     * */
    this.maxLiveOffsetErrorMs = 20;

    /** @private {number} */
    this.proportionalControlFactorMs_ = 0.1;

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

  /**
   * Constrain the playback rate to the configured limits.
   * @private
   * @param {number} value
   * @param {number} min
   * @param {number} max
   * @return {number}
   */
  constrainValue_(value, min, max) {
    return Math.max(min, Math.min(max, value));
  }

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

    let newRate = 1.0;
    const targetLatency = this.config_.targetLiveLatencyOverride > 0 ?
      this.config_.targetLiveLatencyOverride / 1000 :
      this.getTargetLatency_();
    if (targetLatency < 0) {
      return newRate;
    }
    const latency = this.getLatency_();
    const liveOffsetError = latency - targetLatency;
    if (Math.abs(liveOffsetError) > (this.maxLiveOffsetErrorMs / 1000)) {
      const calculatedRate =
        1.0 + this.proportionalControlFactorMs_ * liveOffsetError;
      newRate = this.constrainValue_(calculatedRate, minRate, maxRate);
    }
    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;
  }

  /**
   * @return {number}
   * @private
   */
  getTargetLatency_() {
    let latency = -1;
    const serviceDescription = this.playerInterface_.getServiceDescription();
    if (serviceDescription && serviceDescription.playbackRate) {
      latency = serviceDescription.latency.target / 1000;
    }
    return latency;
  }

  /**
    * @return {number}
    * @private
    */
  getLatency_() {
    const prftInfo = this.playerInterface_.getPresentationLatencyInfo();
    return prftInfo ? prftInfo.latency : this.getTargetLatency_();
  }
};

/**
 * @typedef {{
 *   getBufferEnd: function():number,
 *   getPlayRate: function():number,
 *   getPresentationLatencyInfo: (function():
 *   shaka.media.StreamingEngine.PresentationLatencyInfo|null),
 *   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():
 * shaka.media.StreamingEngine.PresentationLatencyInfo|null
 * } getPresentationLatencyInfo
 *   Get the presentation latency
 * @property {function(number)} trickPlay
 *   Called when an event occurs that should be sent to the app.
 */
shaka.media.LiveCatchUpController.PlayerInterface;