diff --git a/contrib/akamai/controlbar/ControlBar.js b/contrib/akamai/controlbar/ControlBar.js index cc418f941d..3864dbdeea 100644 --- a/contrib/akamai/controlbar/ControlBar.js +++ b/contrib/akamai/controlbar/ControlBar.js @@ -359,7 +359,7 @@ var ControlBar = function (dashjsMediaPlayer, displayUTCTimeCodes) { }; var seekLive = function () { - self.player.seek(self.player.duration()); + self.player.seekToOriginalLive(); }; //************************************************************************************ diff --git a/index.d.ts b/index.d.ts index f50326db6f..5bb4a0f634 100644 --- a/index.d.ts +++ b/index.d.ts @@ -228,7 +228,6 @@ declare namespace dashjs { liveCatchup?: { maxDrift?: number; playbackRate?: number; - latencyThreshold?: number, playbackBufferMin?: number, enabled?: boolean mode?: string @@ -467,6 +466,8 @@ declare namespace dashjs { seek(value: number): void; + seekToOriginalLive(): void; + setPlaybackRate(value: number): void; getPlaybackRate(): number; diff --git a/samples/low-latency/testplayer/main.css b/samples/low-latency/testplayer/main.css index 7c1c9413ca..9591c50015 100644 --- a/samples/low-latency/testplayer/main.css +++ b/samples/low-latency/testplayer/main.css @@ -1,5 +1,11 @@ +.videoContainer{ + position: relative; +} + video { width: 100%; + height: auto; + margin: auto; } #manifest { @@ -29,4 +35,12 @@ video { #metric-chart { max-height: 400px; min-height: 400px; +} + +.video-controller { + margin-top: -5px !important; +} + +.dash-video-player { + background: #000000; } \ No newline at end of file diff --git a/samples/low-latency/testplayer/main.js b/samples/low-latency/testplayer/main.js index d800765dee..1da2d3f11e 100644 --- a/samples/low-latency/testplayer/main.js +++ b/samples/low-latency/testplayer/main.js @@ -2,6 +2,7 @@ var METRIC_INTERVAL = 300; var App = function () { this.player = null; + this.controlbar = null; this.video = null; this.chart = null; this.domElements = { @@ -30,7 +31,6 @@ App.prototype._setDomElements = function () { this.domElements.settings.targetLatency = document.getElementById('target-latency'); this.domElements.settings.maxDrift = document.getElementById('max-drift'); this.domElements.settings.catchupPlaybackRate = document.getElementById('catchup-playback-rate'); - this.domElements.settings.liveCatchupLatencyThreshold = document.getElementById('catchup-threshold'); this.domElements.settings.abrAdditionalInsufficientBufferRule = document.getElementById('abr-additional-insufficient') this.domElements.settings.abrAdditionalDroppedFramesRule = document.getElementById('abr-additional-dropped'); this.domElements.settings.abrAdditionalAbandonRequestRule = document.getElementById('abr-additional-abandon'); @@ -46,7 +46,6 @@ App.prototype._setDomElements = function () { this.domElements.metrics.latencyTag = document.getElementById('latency-tag'); this.domElements.metrics.playbackrateTag = document.getElementById('playbackrate-tag'); this.domElements.metrics.bufferTag = document.getElementById('buffer-tag'); - this.domElements.metrics.catchupThresholdTag = document.getElementById('catchup-threshold-tag'); this.domElements.metrics.sec = document.getElementById('sec'); this.domElements.metrics.min = document.getElementById('min'); this.domElements.metrics.videoMaxIndex = document.getElementById('video-max-index'); @@ -71,6 +70,8 @@ App.prototype._load = function () { this._registerDashEventHandler(); this._applyParameters(); this.player.initialize(this.video, url, true); + this.controlbar = new ControlBar(this.player); + this.controlbar.initialize(); } App.prototype._applyParameters = function () { @@ -89,7 +90,6 @@ App.prototype._applyParameters = function () { liveCatchup: { maxDrift: settings.maxDrift, playbackRate: settings.catchupPlaybackRate, - latencyThreshold: settings.liveCatchupLatencyThreshold, mode: settings.catchupMechanism }, abr: { @@ -133,9 +133,6 @@ App.prototype._adjustSettingsByUrlParameters = function () { if (params.catchupPlaybackRate !== undefined) { this.domElements.settings.catchupPlaybackRate.value = parseFloat(params.catchupPlaybackRate).toFixed(1); } - if (params.liveCatchupLatencyThreshold !== undefined) { - this.domElements.settings.liveCatchupLatencyThreshold.value = parseFloat(params.liveCatchupLatencyThreshold).toFixed(0); - } if (params.abrAdditionalInsufficientBufferRule !== undefined) { this.domElements.settings.abrAdditionalInsufficientBufferRule.checked = params.abrAdditionalInsufficientBufferRule === 'true'; } @@ -165,7 +162,6 @@ App.prototype._getCurrentSettings = function () { var targetLatency = parseFloat(this.domElements.settings.targetLatency.value, 10); var maxDrift = parseFloat(this.domElements.settings.maxDrift.value, 10); var catchupPlaybackRate = parseFloat(this.domElements.settings.catchupPlaybackRate.value, 10); - var liveCatchupLatencyThreshold = parseFloat(this.domElements.settings.liveCatchupLatencyThreshold.value, 10); var abrAdditionalInsufficientBufferRule = this.domElements.settings.abrAdditionalInsufficientBufferRule.checked; var abrAdditionalDroppedFramesRule = this.domElements.settings.abrAdditionalDroppedFramesRule.checked; var abrAdditionalAbandonRequestRule = this.domElements.settings.abrAdditionalAbandonRequestRule.checked; @@ -178,7 +174,6 @@ App.prototype._getCurrentSettings = function () { targetLatency, maxDrift, catchupPlaybackRate, - liveCatchupLatencyThreshold, abrGeneral, abrAdditionalInsufficientBufferRule, abrAdditionalDroppedFramesRule, @@ -344,8 +339,6 @@ App.prototype._startIntervalHandler = function () { var currentBuffer = dashMetrics.getCurrentBufferLevel('video'); self.domElements.metrics.bufferTag.innerHTML = currentBuffer + ' secs'; - self.domElements.metrics.catchupThresholdTag.innerHTML = settings.streaming.liveCatchup.latencyThreshold + ' secs'; - var d = new Date(); var seconds = d.getSeconds(); self.domElements.metrics.sec.innerHTML = (seconds < 10 ? '0' : '') + seconds; diff --git a/samples/low-latency/testplayer/testplayer.html b/samples/low-latency/testplayer/testplayer.html index 0bdc804005..3b82677c94 100644 --- a/samples/low-latency/testplayer/testplayer.html +++ b/samples/low-latency/testplayer/testplayer.html @@ -4,15 +4,17 @@ Low latency streaming - Testplayer - - - + + + + + @@ -71,11 +73,6 @@
General
-
- Live catchup latency threshold (sec): - -
ABR - General
@@ -243,8 +240,39 @@

Export settings

-
- +
+
+ +
+
+ +
+ 00:00:00 +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ 00:00:00 +
+
+
+
+
+
+
+
@@ -267,9 +295,6 @@
Wall Clock reference time
Playback rate:
-
Live catchup latency threshold:
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(); }