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;