diff --git a/src/core/Settings.js b/src/core/Settings.js
index 499998c47a..fc669fe919 100644
--- a/src/core/Settings.js
+++ b/src/core/Settings.js
@@ -140,7 +140,6 @@ import Events from './events/Events';
* maxDrift: NaN,
* playbackRate: NaN,
* playbackBufferMin: 0.5,
- * latencyThreshold: 60,
* enabled: false,
* mode: Constants.LIVE_CATCHUP_MODE_DEFAULT
* },
@@ -462,15 +461,6 @@ import Events from './events/Events';
* Set it to NaN to turn off live catch up feature.
*
* Note: Catch-up mechanism is only applied when playing low latency live streams.
- * @property {number} [latencyThreshold=60]
- * Use this parameter to set the maximum threshold for which live catch up is applied.
- *
- * For instance, if this value is set to 8 seconds, then live catchup is only applied if the current live latency is equal or below 8 seconds.
- *
- * The reason behind this parameter is to avoid an increase of the playback rate if the user seeks within the DVR window.
- *
- * If no value is specified catchup mode will always be applied
- *
* @property {number} [playbackBufferMin=NaN]
* Use this parameter to specify the minimum buffer which is used for LoL+ based playback rate reduction.
*
@@ -846,7 +836,6 @@ function Settings() {
playbackRate: NaN,
playbackBufferMin: 0.5,
enabled: null,
- latencyThreshold: 60,
mode: Constants.LIVE_CATCHUP_MODE_DEFAULT
},
lastBitrateCachingInfo: {
diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js
index 89b68c4cfb..7f3a56afcf 100644
--- a/src/streaming/MediaPlayer.js
+++ b/src/streaming/MediaPlayer.js
@@ -529,7 +529,7 @@ function MediaPlayer() {
throw PLAYBACK_NOT_INITIALIZED_ERROR;
}
if (!autoPlay || (isPaused() && playbackInitialized)) {
- playbackController.play();
+ playbackController.play(true);
}
}
@@ -565,7 +565,8 @@ function MediaPlayer() {
* Sets the currentTime property of the attached video element. If it is a live stream with a
* timeShiftBufferLength, then the DVR window offset will be automatically calculated.
*
- * @param {number} value - A relative time, in seconds, based on the return value of the {@link module:MediaPlayer#duration duration()} method is expected
+ * @param {number} value - A relative time, in seconds, based on the return value of the {@link module:MediaPlayer#duration duration()} method is expected.
+ * For dynamic streams duration() returns DVRWindow.end - DVRWindow.start. Consequently, the value provided to this function should be relative to DVRWindow.start.
* @see {@link module:MediaPlayer#getDVRSeekOffset getDVRSeekOffset()}
* @throws {@link module:MediaPlayer~PLAYBACK_NOT_INITIALIZED_ERROR PLAYBACK_NOT_INITIALIZED_ERROR} if called before initializePlayback function
* @throws {@link Constants#BAD_ARGUMENT_ERROR BAD_ARGUMENT_ERROR} if called with an invalid argument, not number type or is NaN.
@@ -584,7 +585,18 @@ function MediaPlayer() {
}
let s = playbackController.getIsDynamic() ? getDVRSeekOffset(value) : value;
- playbackController.seek(s);
+ playbackController.seek(s, false, false, true);
+ }
+
+ /**
+ * Seeks back to the original live edge (live edge as calculated at playback start). Only applies to live streams, for VoD streams this call will be ignored.
+ */
+ function seekToOriginalLive() {
+ if (!playbackInitialized || !isDynamic()) {
+ return;
+ }
+
+ playbackController.seekToOriginalLive();
}
/**
@@ -763,7 +775,7 @@ function MediaPlayer() {
return 0;
}
- let liveDelay = playbackController.getLiveDelay();
+ let liveDelay = playbackController.getOriginalLiveDelay();
let val = metric.range.start + value;
@@ -785,7 +797,7 @@ function MediaPlayer() {
throw PLAYBACK_NOT_INITIALIZED_ERROR;
}
- return playbackController.getLiveDelay();
+ return playbackController.getOriginalLiveDelay();
}
/**
@@ -2064,7 +2076,6 @@ function MediaPlayer() {
streamController,
playbackController,
mediaPlayerModel,
- dashMetrics,
videoModel,
settings
})
@@ -2312,6 +2323,7 @@ function MediaPlayer() {
isDynamic,
getLowLatencyModeEnabled,
seek,
+ seekToOriginalLive,
setPlaybackRate,
getPlaybackRate,
setMute,
diff --git a/src/streaming/controllers/CatchupController.js b/src/streaming/controllers/CatchupController.js
index f878113785..eb9c0a900b 100644
--- a/src/streaming/controllers/CatchupController.js
+++ b/src/streaming/controllers/CatchupController.js
@@ -48,7 +48,6 @@ function CatchupController() {
streamController,
playbackController,
mediaPlayerModel,
- dashMetrics,
playbackStalled,
logger;
@@ -77,10 +76,6 @@ function CatchupController() {
playbackController = config.playbackController;
}
- if (config.dashMetrics) {
- dashMetrics = config.dashMetrics;
- }
-
if (config.mediaPlayerModel) {
mediaPlayerModel = config.mediaPlayerModel;
}
@@ -206,7 +201,7 @@ function CatchupController() {
deltaLatency > maxDrift) {
logger.info('[CatchupController]: Low Latency catchup mechanism. Latency too high, doing a seek to live point');
isCatchupSeekInProgress = true;
- _seekToLive();
+ playbackController.seekToCurrentLive(true, false);
}
// try to reach the target latency by adjusting the playback rate
@@ -250,8 +245,7 @@ function CatchupController() {
*/
function _shouldStartCatchUp() {
try {
- const latencyThreshold = mediaPlayerModel.getLiveCatchupLatencyThreshold();
- if (!playbackController.getTime() > 0 || isCatchupSeekInProgress || (!isNaN(latencyThreshold) && playbackController.getCurrentLiveLatency() >= latencyThreshold)) {
+ if (!playbackController.getTime() > 0 || isCatchupSeekInProgress) {
return false;
}
@@ -413,19 +407,6 @@ function CatchupController() {
return newRate
}
- /**
- * Seek to live edge
- */
- function _seekToLive() {
- const type = streamController && streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO;
- const DVRMetrics = dashMetrics.getCurrentDVRInfo(type);
- const DVRWindow = DVRMetrics ? DVRMetrics.range : null;
-
- if (DVRWindow && !isNaN(DVRWindow.end)) {
- playbackController.seek(DVRWindow.end - playbackController.getLiveDelay(), true, false);
- }
- }
-
instance = {
reset,
setConfig,
diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js
index 8ce9da6f4a..21abbbc03d 100644
--- a/src/streaming/controllers/PlaybackController.js
+++ b/src/streaming/controllers/PlaybackController.js
@@ -53,6 +53,7 @@ function PlaybackController() {
timelineConverter,
wallclockTimeIntervalId,
liveDelay,
+ originalLiveDelay,
streamInfo,
isDynamic,
playOnceInitialized,
@@ -80,6 +81,7 @@ function PlaybackController() {
pause();
playOnceInitialized = false;
liveDelay = 0;
+ originalLiveDelay = 0;
availabilityStartTime = 0;
manifestUpdateInProgress = false;
availabilityTimeComplete = true;
@@ -175,8 +177,11 @@ function PlaybackController() {
/**
* Triggers play() on the video element
*/
- function play() {
+ function play(adjustLiveDelay = false) {
if (streamInfo && videoModel && videoModel.getElement()) {
+ if (adjustLiveDelay && isDynamic) {
+ _adjustLiveDelayAfterUserInteraction(getTime());
+ }
videoModel.play();
} else {
playOnceInitialized = true;
@@ -197,8 +202,9 @@ function PlaybackController() {
* @param {number} time
* @param {boolean} stickToBuffered
* @param {boolean} internal
+ * @param {boolean} adjustLiveDelay
*/
- function seek(time, stickToBuffered, internal) {
+ function seek(time, stickToBuffered = false, internal = false, adjustLiveDelay = false) {
if (!streamInfo || !videoModel) return;
let currentTime = !isNaN(seekTarget) ? seekTarget : videoModel.getTime();
@@ -210,9 +216,72 @@ function PlaybackController() {
seekTarget = time;
}
logger.info('Requesting seek to time: ' + time + (internalSeek ? ' (internal)' : ''));
+
+ // We adjust the current latency. If catchup is enabled we will maintain this new latency
+ if (isDynamic && adjustLiveDelay) {
+ _adjustLiveDelayAfterUserInteraction(time);
+ }
+
videoModel.setCurrentTime(time, stickToBuffered);
}
+ /**
+ * Seeks back to the live edge as defined by the originally calculated live delay
+ * @param {boolean} stickToBuffered
+ * @param {boolean} internal
+ * @param {boolean} adjustLiveDelay
+ */
+ function seekToOriginalLive(stickToBuffered = false, internal = false, adjustLiveDelay = false) {
+ const dvrWindowEnd = _getDvrWindowEnd();
+
+ if (dvrWindowEnd === 0) {
+ return;
+ }
+
+ liveDelay = originalLiveDelay;
+ const seektime = dvrWindowEnd - liveDelay;
+
+ seek(seektime, stickToBuffered, internal, adjustLiveDelay);
+ }
+
+ /**
+ * Seeks to the live edge as currently defined by liveDelay
+ * @param {boolean} stickToBuffered
+ * @param {boolean} internal
+ * @param {boolean} adjustLiveDelay
+ */
+ function seekToCurrentLive(stickToBuffered = false, internal = false, adjustLiveDelay = false) {
+ const dvrWindowEnd = _getDvrWindowEnd();
+
+ if (dvrWindowEnd === 0) {
+ return;
+ }
+
+ const seektime = dvrWindowEnd - liveDelay;
+
+ seek(seektime, stickToBuffered, internal, adjustLiveDelay);
+ }
+
+ function _getDvrWindowEnd() {
+ if (!streamInfo || !videoModel || !isDynamic) {
+ return;
+ }
+
+ const type = streamController && streamController.hasVideoTrack() ? Constants.VIDEO : Constants.AUDIO;
+ const dvrInfo = dashMetrics.getCurrentDVRInfo(type);
+
+ return dvrInfo && dvrInfo.range ? dvrInfo.range.end : 0;
+ }
+
+
+ function _adjustLiveDelayAfterUserInteraction(time) {
+ const now = new Date(timelineConverter.getClientReferenceTime());
+ const period = adapter.getRegularPeriods()[0];
+ const nowAsPresentationTime = timelineConverter.calcPresentationTimeFromWallTime(now, period);
+
+ liveDelay = nowAsPresentationTime - time;
+ }
+
/**
* Returns current time of video element
* @return {number|null}
@@ -302,13 +371,20 @@ function PlaybackController() {
}
/**
- * Returns the computed live delay
+ * Returns the current live delay. A seek triggered by the user adjusts this value.
* @return {number}
*/
function getLiveDelay() {
return liveDelay;
}
+ /**
+ * Returns the original live delay as calculated at playback start
+ */
+ function getOriginalLiveDelay() {
+ return originalLiveDelay;
+ }
+
/**
* Returns the current live latency
* @return {number}
@@ -385,6 +461,8 @@ function PlaybackController() {
ret = delay;
}
liveDelay = ret;
+ originalLiveDelay = ret;
+
return ret;
}
@@ -698,6 +776,7 @@ function PlaybackController() {
if (minDelay > liveDelay) {
logger.warn('Browser does not support fetch API with StreamReader. Increasing live delay to be 20% higher than segment duration:', minDelay.toFixed(2));
liveDelay = minDelay;
+ originalLiveDelay = minDelay;
}
}
}
@@ -815,6 +894,7 @@ function PlaybackController() {
getStreamController,
computeAndSetLiveDelay,
getLiveDelay,
+ getOriginalLiveDelay,
getCurrentLiveLatency,
play,
isPaused,
@@ -823,6 +903,8 @@ function PlaybackController() {
isSeeking,
getStreamEndTime,
seek,
+ seekToOriginalLive,
+ seekToCurrentLive,
reset,
updateCurrentTime,
getAvailabilityStartTime
diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js
index 62deddd709..945b1554f1 100644
--- a/src/streaming/controllers/StreamController.js
+++ b/src/streaming/controllers/StreamController.js
@@ -734,7 +734,7 @@ function StreamController() {
* @private
*/
function _onLiveDelaySettingUpdated() {
- if (adapter.getIsDynamic() && playbackController.getLiveDelay() !== 0) {
+ if (adapter.getIsDynamic() && playbackController.getOriginalLiveDelay() !== 0) {
const streamsInfo = adapter.getStreamsInfo()
if (streamsInfo.length > 0) {
const manifestInfo = streamsInfo[0].manifestInfo;
@@ -1031,7 +1031,7 @@ function StreamController() {
const dvrInfo = dashMetrics.getCurrentDVRInfo();
const liveEdge = dvrInfo && dvrInfo.range ? dvrInfo.range.end : 0;
// we are already in the right start period. so time should not be smaller than period@start and should not be larger than period@end
- startTime = liveEdge - playbackController.getLiveDelay();
+ startTime = liveEdge - playbackController.getOriginalLiveDelay();
// If start time in URI, take min value between live edge time and time from URI (capped by DVR window range)
const dvrWindow = dvrInfo ? dvrInfo.range : null;
if (dvrWindow) {
diff --git a/src/streaming/models/MediaPlayerModel.js b/src/streaming/models/MediaPlayerModel.js
index 2c2a1b5e7d..c3ed6be463 100644
--- a/src/streaming/models/MediaPlayerModel.js
+++ b/src/streaming/models/MediaPlayerModel.js
@@ -112,26 +112,6 @@ function MediaPlayerModel() {
return playbackController.getInitialCatchupModeActivated();
}
- /**
- * Returns the threshold for which to apply the catchup logic
- * @return {number}
- */
- function getLiveCatchupLatencyThreshold() {
- try {
- const liveCatchupLatencyThreshold = settings.get().streaming.liveCatchup.latencyThreshold;
- const liveDelay = playbackController.getLiveDelay();
-
- if (liveCatchupLatencyThreshold !== null && !isNaN(liveCatchupLatencyThreshold)) {
- return Math.max(liveCatchupLatencyThreshold, liveDelay);
- }
-
- return NaN;
-
- } catch (e) {
- return NaN;
- }
- }
-
/**
* Returns the min,max or initial bitrate for a specific media type.
* @param {string} field
@@ -193,7 +173,7 @@ function MediaPlayerModel() {
}
/**
- * Returns the retry interbal for a specific media type
+ * Returns the retry interval for a specific media type
* @param type
* @return {number}
*/
@@ -209,7 +189,6 @@ function MediaPlayerModel() {
instance = {
getCatchupMaxDrift,
getCatchupModeEnabled,
- getLiveCatchupLatencyThreshold,
getStableBufferTime,
getInitialBufferLevel,
getRetryAttemptsForType,
diff --git a/test/unit/mocks/PlaybackControllerMock.js b/test/unit/mocks/PlaybackControllerMock.js
index b4be91ffec..815534cafd 100644
--- a/test/unit/mocks/PlaybackControllerMock.js
+++ b/test/unit/mocks/PlaybackControllerMock.js
@@ -106,6 +106,10 @@ class PlaybackControllerMock {
return 15;
}
+ getOriginalLiveDelay() {
+ return 15;
+ }
+
reset() {
this.setup();
}