From 6b772c4229e3ae4210ef80a4410791fdfce32ac3 Mon Sep 17 00:00:00 2001 From: Bertrand Berthelot Date: Mon, 29 Jun 2020 15:26:13 +0200 Subject: [PATCH] Add processing of MPD time anchor (#t) according to Annex C of MPEG DASH spec --- .../controllers/PlaybackController.js | 59 +++++--- src/streaming/controllers/StreamController.js | 7 +- ...reaming.controllers.PlaybackControllers.js | 129 ++++++++++++++++-- 3 files changed, 164 insertions(+), 31 deletions(-) diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index fa2a8b3b35..cba61e391b 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -106,13 +106,36 @@ function PlaybackController() { if (streamSwitch && isNaN(streamSeekTime)) return; // Seek new stream in priority order: - // - at seek time (streamSeekTime) that required period switch + // - at seek time (streamSeekTime) when switching period // - at start time provided in URI parameters // - at stream/period start time (for static streams) or live start time (for dynamic streams) let startTime = streamSeekTime; if (isNaN(startTime)) { - startTime = isDynamic ? e.liveStartTime : e.streamInfo.start; - startTime = Math.max(startTime, getStartTimeFromUri()); + if (isDynamic) { + // For dynamic stream, start by default at (live edge - live delay) + startTime = e.liveStartTime; + // If start time in URI, take min value between live edge time and time from URI (if in DVR window range) + const dvrInfo = dashMetrics.getCurrentDVRInfo(); + const dvrWindow = dvrInfo ? dvrInfo.range : null; + if (dvrWindow) { + const startTimeFromUri = getStartTimeFromUriParameters(dvrWindow.start); + if (!isNaN(startTimeFromUri) && startTimeFromUri >= dvrWindow.start) { + logger.info('Start time from URI parameters: ' + startTimeFromUri); + startTime = Math.min(startTime, startTimeFromUri); + } + } + } else { + // For static stream, start by default at period start + startTime = streamInfo.start; + // If start time in URI, take max value between period start and time from URI (if in period range) + const startTimeFromUri = getStartTimeFromUriParameters(streamInfo.start); + if (!isNaN(startTimeFromUri) && startTimeFromUri < (startTime + streamInfo.duration)) { + logger.info('Start time from URI parameters: ' + startTimeFromUri); + startTime = Math.max(startTime, startTimeFromUri); + } + } + + // Check if not seeking at current time if (startTime === videoModel.getTime()) return; } @@ -348,25 +371,23 @@ function PlaybackController() { } } - function getStartTimeFromUriParameters() { + function getStartTimeFromUriParameters(periodStart) { const fragData = uriFragmentModel.getURIFragmentData(); - let uriParameters; - if (fragData) { - uriParameters = {}; - const r = parseInt(fragData.r, 10); - if (r >= 0 && streamInfo && r < streamInfo.manifestInfo.DVRWindowSize && fragData.t === null) { - fragData.t = Math.max(Math.floor(Date.now() / 1000) - streamInfo.manifestInfo.DVRWindowSize, (streamInfo.manifestInfo.availableFrom.getTime() / 1000) + streamInfo.start) + r; - } - uriParameters.fragS = parseFloat(fragData.s); - uriParameters.fragT = parseFloat(fragData.t); + if (!fragData || !fragData.t) { + return NaN; } - return uriParameters; - } - function getStartTimeFromUri() { - const uriParameters = getStartTimeFromUriParameters(); - let start = !isNaN(uriParameters.fragS) ? uriParameters.fragS : (!isNaN(uriParameters.fragT) ? uriParameters.fragT : -1); - return start; + let startTime = NaN; + + // Consider only start time of MediaRange + // TODO: consider end time of MediaRange to stop playback at provided end time + fragData.t = fragData.t.split(',')[0]; + + // t is relative to period start + // posix notation (t=posix:xxx) = absolute time range as number of seconds since 01-01-1970 + startTime = (fragData.t.indexOf('posix:') !== -1) ? parseInt(fragData.t.substring(6)) : (periodStart + parseInt(fragData.t)); + + return startTime; } function getActualPresentationTime(currentTime) { diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index 252038e8fa..610cd8448c 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -660,11 +660,10 @@ function StreamController() { } // we need to figure out what the correct starting period is - const startTimeFormUriParameters = playbackController.getStartTimeFromUriParameters(); let initialStream = null; - if (startTimeFormUriParameters) { - const initialTime = !isNaN(startTimeFormUriParameters.fragS) ? startTimeFormUriParameters.fragS : startTimeFormUriParameters.fragT; - initialStream = getStreamForTime(initialTime); + const startTimeFormUri = playbackController.getStartTimeFromUriParameters(streamsInfo[0].start); + if (!isNaN(startTimeFormUri)) { + initialStream = getStreamForTime(startTimeFormUri); } // For multiperiod streams we should avoid a switch of streams after the seek to the live edge. So we do a calculation of the expected seek time to find the right stream object. if (!initialStream && adapter.getIsDynamic() && streams.length) { diff --git a/test/unit/streaming.controllers.PlaybackControllers.js b/test/unit/streaming.controllers.PlaybackControllers.js index b3696c84fd..e143bf84be 100644 --- a/test/unit/streaming.controllers.PlaybackControllers.js +++ b/test/unit/streaming.controllers.PlaybackControllers.js @@ -85,7 +85,8 @@ describe('PlaybackController', function () { isDynamic: true, availableFrom: new Date() }, - start: 10 + start: 10, + duration: 600 }; playbackController.initialize(streamInfo); @@ -160,13 +161,6 @@ describe('PlaybackController', function () { videoModelMock.ended = true; expect(playbackController.getEnded()).to.equal(videoModelMock.ended); }); - - it('getStartTimeFromUriParameters should return the expected value', function () { - uriFragmentModelMock.setURIFragmentData({t: 18.2}); - const uriParameters = playbackController.getStartTimeFromUriParameters(); - expect(uriParameters.fragT).to.exist; // jshint ignore:line - expect(uriParameters.fragT).to.equal(18.2); - }); }); describe('video event handler', function () { @@ -325,6 +319,125 @@ describe('PlaybackController', function () { videoModelMock.fireEvent('waiting'); }); }); + }); + + describe('start time', function () { + let expectedSeekTime; + let doneFn; + + let staticStreamInfo = { manifestInfo: { isDynamic: false }, start: 10, duration: 600 }; + let dynamicStreamInfo = { manifestInfo: { isDynamic: true }, start: 0, duration: Infinity }; + let dvrWindowRange = { start: 10, end: 90 }; + + let onPlaybackSeeking = function (e) { + eventBus.off(Events.PLAYBACK_SEEKING, onPlaybackSeeking); + expect(e.seekTime).to.equal(expectedSeekTime); + doneFn(); + }; + + beforeEach(function () { + videoModelMock.time = -1; + eventBus.on(Events.PLAYBACK_SEEKING, onPlaybackSeeking, this); + }); + + it('should start static stream at period start', function (done) { + doneFn = done; + + expectedSeekTime = staticStreamInfo.start; + + playbackController.initialize(staticStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {}); + }); + + it('should start static stream at #t', function (done) { + doneFn = done; + + let uriStartTime = 10; + uriFragmentModelMock.setURIFragmentData({t: uriStartTime.toString()}); + + expectedSeekTime = staticStreamInfo.start + uriStartTime; + + playbackController.initialize(staticStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {}); + }); + + it('should start static stream at period start if #t is beyond period end', function (done) { + doneFn = done; + + let uriStartTime = 700; + uriFragmentModelMock.setURIFragmentData({t: uriStartTime.toString()}); + + expectedSeekTime = staticStreamInfo.start; + + playbackController.initialize(staticStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {}); + }); + it('should start dynamic stream at live start time', function (done) { + doneFn = done; + + let liveStartTime = 90; + + expectedSeekTime = liveStartTime; + + playbackController.initialize(dynamicStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {liveStartTime: liveStartTime}); + }); + + it('should start dynamic stream at #t', function (done) { + doneFn = done; + + let liveStartTime = 70; + dashMetricsMock.addDVRInfo('video', Date.now(), null, dvrWindowRange); + let uriStartTime = 50; + uriFragmentModelMock.setURIFragmentData({t: uriStartTime.toString()}); + + expectedSeekTime = dvrWindowRange.start + uriStartTime; + + playbackController.initialize(dynamicStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {liveStartTime: liveStartTime}); + }); + + it('should start dynamic stream at #t=posix:', function (done) { + doneFn = done; + + let liveStartTime = 70; + dashMetricsMock.addDVRInfo('video', Date.now(), null, dvrWindowRange); + let uriStartTime = 50; + uriFragmentModelMock.setURIFragmentData({t: 'posix:' + uriStartTime.toString()}); + + expectedSeekTime = uriStartTime; + + playbackController.initialize(dynamicStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {liveStartTime: liveStartTime}); + }); + + it('should start dynamic stream at live start time if #t=posix is before DVR window range', function (done) { + doneFn = done; + + let liveStartTime = 70; + dashMetricsMock.addDVRInfo('video', Date.now(), null, dvrWindowRange); + let uriStartTime = 0; + uriFragmentModelMock.setURIFragmentData({t: 'posix:' + uriStartTime.toString()}); + + expectedSeekTime = liveStartTime; + + playbackController.initialize(dynamicStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {liveStartTime: liveStartTime}); + }); + + it('should start dynamic stream at live start time if #t=posix is beyond live start time', function (done) { + doneFn = done; + + let liveStartTime = 70; + dashMetricsMock.addDVRInfo('video', Date.now(), null, dvrWindowRange); + let uriStartTime = 80; + uriFragmentModelMock.setURIFragmentData({t: 'posix:' + uriStartTime.toString()}); + + expectedSeekTime = liveStartTime; + + playbackController.initialize(dynamicStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {liveStartTime: liveStartTime}); + }); }); });