From d037daed572d9a287c73fd4fc19880a07b37d807 Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Fri, 18 Feb 2022 15:57:06 +0000 Subject: [PATCH 01/18] Calculate and apply time offsets from ProducerReferenceTime when setting liveDelay from ServiceDescription --- src/dash/DashAdapter.js | 23 ++++++ src/dash/constants/DashConstants.js | 4 + src/dash/models/DashManifestModel.js | 42 ++++++++++- src/dash/vo/ProducerReferenceTime.js | 46 ++++++++++++ .../controllers/PlaybackController.js | 40 +++++++++- src/streaming/controllers/StreamController.js | 56 +++++++++++++- test/unit/dash.DashAdapter.js | 17 ++++- test/unit/dash.constants.DashConstants.js | 4 + test/unit/dash.models.DashManifestModel.js | 74 +++++++++++++++++++ ...reaming.controllers.PlaybackControllers.js | 50 ++++++++++--- 10 files changed, 338 insertions(+), 18 deletions(-) create mode 100644 src/dash/vo/ProducerReferenceTime.js diff --git a/src/dash/DashAdapter.js b/src/dash/DashAdapter.js index 67d23f7e92..9786085efb 100644 --- a/src/dash/DashAdapter.js +++ b/src/dash/DashAdapter.js @@ -384,6 +384,28 @@ function DashAdapter() { return realAdaptation; } + /** + * Returns the ProducerReferenceTime as saved in the DashManifestModel if present + * @param {object} streamInfo + * @param {object} mediaInfo + * @returns {object} producerReferenceTime + * @memberOf module:DashAdapter + * @instance + */ + function getProducerReferenceTimes(streamInfo, mediaInfo) { + let id, realAdaptation; + + const selectedVoPeriod = getPeriodForStreamInfo(streamInfo, voPeriods); + id = mediaInfo ? mediaInfo.id : null; + + if (voPeriods.length > 0 && selectedVoPeriod) { + realAdaptation = id ? dashManifestModel.getAdaptationForId(id, voPeriods[0].mpd.manifest, selectedVoPeriod.index) : dashManifestModel.getAdaptationForIndex(mediaInfo ? mediaInfo.index : null, voPeriods[0].mpd.manifest, selectedVoPeriod.index); + } + + if (!realAdaptation) return []; + return dashManifestModel.getProducerReferenceTimesForAdaptation(realAdaptation); + } + /** * Return all EssentialProperties of a Representation * @param {object} representation @@ -1165,6 +1187,7 @@ function DashAdapter() { getAllMediaInfoForType, getAdaptationForType, getRealAdaptation, + getProducerReferenceTimes, getRealPeriodByIndex, getEssentialPropertiesForRepresentation, getVoRepresentations, diff --git a/src/dash/constants/DashConstants.js b/src/dash/constants/DashConstants.js index 6cd85535df..fe01bab6d5 100644 --- a/src/dash/constants/DashConstants.js +++ b/src/dash/constants/DashConstants.js @@ -90,6 +90,7 @@ class DashConstants { this.ESSENTIAL_PROPERTY = 'EssentialProperty'; this.SUPPLEMENTAL_PROPERTY = 'SupplementalProperty'; this.INBAND_EVENT_STREAM = 'InbandEventStream'; + this.PRODUCER_REFERENCE_TIME = 'ProducerReferenceTime'; this.ACCESSIBILITY = 'Accessibility'; this.ROLE = 'Role'; this.RATING = 'Rating'; @@ -98,6 +99,7 @@ class DashConstants { this.LANG = 'lang'; this.VIEWPOINT = 'Viewpoint'; this.ROLE_ASARRAY = 'Role_asArray'; + this.PRODUCERREFERENCETIME_ASARRAY = 'ProducerReferenceTime_asArray'; this.ACCESSIBILITY_ASARRAY = 'Accessibility_asArray'; this.AUDIOCHANNELCONFIGURATION_ASARRAY = 'AudioChannelConfiguration_asArray'; this.CONTENTPROTECTION_ASARRAY = 'ContentProtection_asArray'; @@ -135,6 +137,8 @@ class DashConstants { this.PUBLISH_TIME = 'publishTime'; this.ORIGINAL_PUBLISH_TIME = 'originalPublishTime'; this.ORIGINAL_MPD_ID = 'mpdId'; + this.WALL_CLOCK_TIME = 'wallClockTime'; + this.PRESENTATION_TIME = 'presentationTime'; } constructor () { diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index 6de12eb66f..5fef185629 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -38,6 +38,7 @@ import UTCTiming from '../vo/UTCTiming'; import Event from '../vo/Event'; import BaseURL from '../vo/BaseURL'; import EventStream from '../vo/EventStream'; +import ProducerReferenceTime from '../vo/ProducerReferenceTime'; import ObjectUtils from '../../streaming/utils/ObjectUtils'; import URLUtils from '../../streaming/utils/URLUtils'; import FactoryMaker from '../../core/FactoryMaker'; @@ -162,6 +163,43 @@ function DashManifestModel() { return getIsTypeOf(adaptation, Constants.IMAGE); } + function getProducerReferenceTimesForAdaptation(adaptation) { + const prtArray = adaptation && adaptation.hasOwnProperty(DashConstants.PRODUCERREFERENCETIME_ASARRAY) ? adaptation[DashConstants.PRODUCERREFERENCETIME_ASARRAY] : []; + const prtsForAdaptation = []; + + // Unlikely to have multiple ProducerReferenceTimes. + prtArray.forEach((prt) => { + const entry = new ProducerReferenceTime(); + + if (prt.hasOwnProperty(DashConstants.ID)) { + entry[DashConstants.ID] = prt[DashConstants.ID]; + } else { + // Ignore. Missing mandatory attribute + return; + } + + if (prt.hasOwnProperty(DashConstants.WALL_CLOCK_TIME)) { + entry[DashConstants.WALL_CLOCK_TIME] = prt[DashConstants.WALL_CLOCK_TIME]; + } else { + // Ignore. Missing mandatory attribute + return; + } + + if (prt.hasOwnProperty(DashConstants.PRESENTATION_TIME)) { + entry[DashConstants.PRESENTATION_TIME] = prt[DashConstants.PRESENTATION_TIME]; + } else { + // Ignore. Missing mandatory attribute + return; + } + + // Not intereseted in other attributes for now + // UTC element contained must be same as that in the MPD + prtsForAdaptation.push(entry); + }) + + return prtsForAdaptation; + } + function getLanguageForAdaptation(adaptation) { let lang = ''; @@ -1116,7 +1154,8 @@ function DashManifestModel() { latency = { target: sd[prop].target, max: sd[prop].max, - min: sd[prop].min + min: sd[prop].min, + referenceId: sd[prop].referenceId }; } else if (prop === DashConstants.SERVICE_DESCRIPTION_PLAYBACK_RATE) { playbackRate = { @@ -1170,6 +1209,7 @@ function DashManifestModel() { getIsTypeOf, getIsText, getIsFragmented, + getProducerReferenceTimesForAdaptation, getLanguageForAdaptation, getViewpointForAdaptation, getRolesForAdaptation, diff --git a/src/dash/vo/ProducerReferenceTime.js b/src/dash/vo/ProducerReferenceTime.js new file mode 100644 index 0000000000..8a66353e55 --- /dev/null +++ b/src/dash/vo/ProducerReferenceTime.js @@ -0,0 +1,46 @@ +/** + * The copyright in this software is being made available under the BSD License, + * included below. This software may be subject to other third party and contributor + * rights, including patent rights, and no such rights are granted under this license. + * + * Copyright (c) 2013, Dash Industry Forum. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * * Neither the name of Dash Industry Forum nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +/** + * @class + * @ignore + */ + class ProducerReferenceTime { + constructor() { + this.id = null; + this.inband = false; + this.type = 'encoder'; + this.applicationScheme = null; + this.wallClockTime = null; + this.presentationTime = NaN; + } +} + +export default ProducerReferenceTime; \ No newline at end of file diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index a51fc64ad7..57be0ef219 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -224,7 +224,7 @@ function PlaybackController() { * @returns {number} object * @memberof PlaybackController# */ - function computeAndSetLiveDelay(fragmentDuration, manifestInfo) { + function computeAndSetLiveDelay(fragmentDuration, timeOffsets, manifestInfo) { let delay, ret, startTime; @@ -238,7 +238,7 @@ function PlaybackController() { // Apply live delay from ServiceDescription if (settings.get().streaming.delay.applyServiceDescription && isNaN(settings.get().streaming.delay.liveDelay) && isNaN(settings.get().streaming.delay.liveDelayFragmentCount)) { - _applyServiceDescription(manifestInfo); + _applyServiceDescription(manifestInfo, timeOffsets); } if (mediaPlayerModel.getLiveDelay()) { @@ -272,7 +272,13 @@ function PlaybackController() { return ret; } - function _applyServiceDescription(manifestInfo) { + /** + * Applys service description Latency and PlaybackRate attributes to liveDelay and catchup settings + * @param {Object} manifestInfo + * @param {Array} producerReferenceTimes - All ProducerReferencesTimes in MPD + * @private + */ + function _applyServiceDescription(manifestInfo, timeOffsets) { if (!manifestInfo || !manifestInfo.serviceDescriptions) { return; } @@ -293,7 +299,7 @@ function PlaybackController() { settings.update({ streaming: { delay: { - liveDelay: llsd.latency.target / 1000 + liveDelay: _calculateOffsetLiveDelay(timeOffsets, llsd.latency), } } }); @@ -321,6 +327,32 @@ function PlaybackController() { } } + /** + * Calculates offset to apply to live delay as described in TS 103 285 Clause 10.20.4 + * @param {Array} timeOffsets + * @param {Object} llsdLatency + * @returns {number} + * @private + */ + function _calculateOffsetLiveDelay(timeOffsets, llsdLatency) { + let to = 0; + let offset = timeOffsets.filter(prt => { + return prt.id === llsdLatency.referenceId; + }); + + // If only one ProducerReferenceTime to generate one TO, then use that regardless of matching ids + if (offset.length === 0) { + to = (timeOffsets.length > 0) ? timeOffsets[0].to : 0; + } else { + // If multiple id matches, use the first but this should be invalid + to = offset[0].to || 0; + } + + // TS 103 285 Clause 10.20.4. 3) Subtract calculated offset from Latency@target converted from milliseconds + // liveLatency does not consider ST@availabilityTimeOffset so leave out that step + return ((llsdLatency.target / 1000) - to) + } + function getAvailabilityStartTime() { return availabilityStartTime; } diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index e1329b13b3..bf639875f8 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -49,6 +49,7 @@ import DashJSError from '../vo/DashJSError'; import Errors from '../../core/errors/Errors'; import EventController from './EventController'; import ConformanceViolationConstants from '../constants/ConformanceViolationConstants'; +import DashConstants from '../../dash/constants/DashConstants'; const PLAYBACK_ENDED_TIMER_INTERVAL = 200; const DVR_WAITING_OFFSET = 2; @@ -344,7 +345,8 @@ function StreamController() { if (adapter.getIsDynamic() && streams.length) { const manifestInfo = streamsInfo[0].manifestInfo; const fragmentDuration = _getFragmentDurationForLiveDelayCalculation(streamsInfo, manifestInfo); - playbackController.computeAndSetLiveDelay(fragmentDuration, manifestInfo); + const timeOffsets = _getLiveDelayTimeOffsets(streamsInfo); + playbackController.computeAndSetLiveDelay(fragmentDuration, timeOffsets, manifestInfo); } // Figure out the correct start time and the correct start period @@ -1150,6 +1152,58 @@ function StreamController() { } } + /** + * Calculates an array of time offsets each with matching ProducerReferenceTime id + * @param {Array} streamInfos + * @returns {Array} + * @private + */ + function _getLiveDelayTimeOffsets(streamInfos) { + try { + + let timeOffsets = []; + + if (streamInfos && streamInfos.length === 1) { + const streamInfo = streamInfos[0]; + const mediaTypes = [Constants.VIDEO, Constants.AUDIO, Constants.TEXT]; + const astInSeconds = adapter.getAvailabilityStartTime() / 1000; + + timeOffsets = mediaTypes + .reduce((acc, mediaType) => { + acc = acc.concat(adapter.getAllMediaInfoForType(streamInfo, mediaType)); + return acc; + }, []) + .reduce((acc, mediaInfo) => { + const prts = adapter.getProducerReferenceTimes(streamInfo, mediaInfo); + prts.forEach((prt) => { + const voRepresentations = adapter.getVoRepresentations(mediaInfo); + if (voRepresentations && voRepresentations.length > 0 && voRepresentations[0].adaptation && voRepresentations[0].segmentInfoType === DashConstants.SEGMENT_TEMPLATE) { + const voRep = voRepresentations[0]; + const d = new Date(prt[DashConstants.WALL_CLOCK_TIME]); + const wallClockTime = d.getTime() / 1000; + // TS 103 285 Clause 10.20.4 + // 1) Calculate PRT0 + // i) take the PRT@presentationTime and subtract any ST@presentationTime + // ii) convert this time to seconds by dividing by ST@timescale + // iii) Add this to start time of period that contains PRT. + const prt0 = wallClockTime - (((prt[DashConstants.PRESENTATION_TIME] - voRep[DashConstants.PRESENTATION_TIME_OFFSET]) / voRep[DashConstants.TIMESCALE]) + streamInfo.start); + // 2) Calculate TO between PRT at the start of MPD timeline and the AST + const to = astInSeconds - prt0; + // 3) Not applicable as liveLatency does not consider ST@availabilityTimeOffset + acc.push({id: prt[DashConstants.ID], to}); + } + }); + + return acc; + }, []) + } + + return timeOffsets; + } catch (e) { + return []; + } + }; + /** * Callback handler after the manifest has been updated. Trigger an update in the adapter and filter unsupported stuff. * Finally attempt UTC sync diff --git a/test/unit/dash.DashAdapter.js b/test/unit/dash.DashAdapter.js index 783b177c6f..c451be8479 100644 --- a/test/unit/dash.DashAdapter.js +++ b/test/unit/dash.DashAdapter.js @@ -44,7 +44,7 @@ const manifest_with_ll_service_description = { ServiceDescription: {}, ServiceDescription_asArray: [{ Scope: { schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019' }, - Latency: { target: 3000, max: 5000, min: 2000 }, + Latency: { target: 3000, max: 5000, min: 2000, referenceId: 7 }, PlaybackRate: { max: 1.5, min: 0.5 } }], Period_asArray: [{ @@ -235,6 +235,20 @@ describe('DashAdapter', function () { expect(realAdaptation).to.be.undefined; // jshint ignore:line }); + it('should return empty array when getProducerReferenceTimes is called and streamInfo parameter is null or undefined', () => { + const producerReferenceTimes = dashAdapter.getProducerReferenceTimes(null, voHelper.getDummyMediaInfo()); + + expect(producerReferenceTimes).to.be.instanceOf(Array); // jshint ignore:line + expect(producerReferenceTimes).to.be.empty; // jshint ignore:line + }); + + it('should return empty array when getProducerReferenceTimes is called and mediaInfo parameter is null or undefined', () => { + const producerReferenceTimes = dashAdapter.getProducerReferenceTimes(voHelper.getDummyStreamInfo(), null); + + expect(producerReferenceTimes).to.be.instanceOf(Array); // jshint ignore:line + expect(producerReferenceTimes).to.be.empty; // jshint ignore:line + }); + it('should return empty array when getUTCTimingSources is called and no period is defined', function () { const timingSources = dashAdapter.getUTCTimingSources(); @@ -470,6 +484,7 @@ describe('DashAdapter', function () { expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.target).equals(3000); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.max).equals(5000); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.min).equals(2000); // jshint ignore:line + expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.referenceId).equals(7); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].playbackRate.max).equals(1.5); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].playbackRate.min).equals(0.5); // jshint ignore:line }); diff --git a/test/unit/dash.constants.DashConstants.js b/test/unit/dash.constants.DashConstants.js index cb221fae32..60dd2a6057 100644 --- a/test/unit/dash.constants.DashConstants.js +++ b/test/unit/dash.constants.DashConstants.js @@ -58,6 +58,7 @@ describe('DashConstants', function () { expect(DashConstants.ESSENTIAL_PROPERTY).to.equal('EssentialProperty'); expect(DashConstants.SUPPLEMENTAL_PROPERTY).to.equal('SupplementalProperty'); expect(DashConstants.INBAND_EVENT_STREAM).to.equal('InbandEventStream'); + expect(DashConstants.PRODUCER_REFERENCE_TIME).to.equal('ProducerReferenceTime'); expect(DashConstants.ACCESSIBILITY).to.equal('Accessibility'); expect(DashConstants.ROLE).to.equal('Role'); expect(DashConstants.RATING).to.equal('Rating'); @@ -66,6 +67,7 @@ describe('DashConstants', function () { expect(DashConstants.LANG).to.equal('lang'); expect(DashConstants.VIEWPOINT).to.equal('Viewpoint'); expect(DashConstants.ROLE_ASARRAY).to.equal('Role_asArray'); + expect(DashConstants.PRODUCERREFERENCETIME_ASARRAY).to.equal('ProducerReferenceTime_asArray'); expect(DashConstants.ACCESSIBILITY_ASARRAY).to.equal('Accessibility_asArray'); expect(DashConstants.AUDIOCHANNELCONFIGURATION_ASARRAY).to.equal('AudioChannelConfiguration_asArray'); expect(DashConstants.CONTENTPROTECTION_ASARRAY).to.equal('ContentProtection_asArray'); @@ -92,5 +94,7 @@ describe('DashConstants', function () { expect(DashConstants.DVB_PRIORITY).to.equal('dvb:priority'); expect(DashConstants.DVB_WEIGHT).to.equal('dvb:weight'); expect(DashConstants.SUGGESTED_PRESENTATION_DELAY).to.equal('suggestedPresentationDelay'); + expect(DashConstants.WALL_CLOCK_TIME).to.equal('wallClockTime'); + expect(DashConstants.PRESENTATION_TIME).to.equal('presentationTime'); }); }); diff --git a/test/unit/dash.models.DashManifestModel.js b/test/unit/dash.models.DashManifestModel.js index 029291fcb7..c38e50e553 100644 --- a/test/unit/dash.models.DashManifestModel.js +++ b/test/unit/dash.models.DashManifestModel.js @@ -1107,6 +1107,80 @@ describe('DashManifestModel', function () { }); }); + describe('getProducerReferenceTimesForAdaptation', () => { + it('returns an empty Array when no ProducerReferenceTimes are present on a node', () => { + const node = {}; + + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + expect(obj).to.be.instanceOf(Array); // jshint ignore:line + expect(obj).to.be.empty; // jshint ignore:line + }); + + it('returns an empty Array where a single ProducerReferenceTime element on a node has missing mandatory attributes', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 4, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:00Z' + // missing presentationTime + } + ] + }; + + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + expect(obj).to.be.instanceOf(Array); // jshint ignore:line + expect(obj).to.be.empty; // jshint ignore:line + }); + + it('returns an Array of ProducerReferenceTime elements with mandatory attributes', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 4, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:04Z', + [DashConstants.PRESENTATION_TIME]: 0 + }, + { + [DashConstants.ID]: 5, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:05Z', + [DashConstants.PRESENTATION_TIME]: 1 + } + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + /* jshint ignore:start */ + expect(obj).to.be.instanceOf(Array); + expect(obj).to.have.lengthOf(2); + expect(obj[0][DashConstants.ID]).to.equal(4); + expect(obj[0][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:04Z'); + expect(obj[0][DashConstants.PRESENTATION_TIME]).to.equal(0); + expect(obj[1][DashConstants.ID]).to.equal(5); + expect(obj[1][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:05Z'); + expect(obj[1][DashConstants.PRESENTATION_TIME]).to.equal(1); + /* jshint ignore:end */ + }); + }); + + it('returns ProducerReferenceTimes with correct default attribute values', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 4, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:04Z', + [DashConstants.PRESENTATION_TIME]: 0 + } + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + expect(obj).to.be.instanceOf(Array); // jshint ignore:line + expect(obj).to.have.lengthOf(1); // jshint ignore:line + expect(obj[0].type).to.equal('encoder'); // jshint ignore:line + }); + describe('getSelectionPriority', () => { it('should return 1 when adaptation is not defined', () => { diff --git a/test/unit/streaming.controllers.PlaybackControllers.js b/test/unit/streaming.controllers.PlaybackControllers.js index a757c19752..3d3a65b2eb 100644 --- a/test/unit/streaming.controllers.PlaybackControllers.js +++ b/test/unit/streaming.controllers.PlaybackControllers.js @@ -114,49 +114,49 @@ describe('PlaybackController', function () { }) it('should return NaN if no values specified', function () { - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.be.NaN; }) it('should return live delay if specified in the settings', function () { settings.update({ streaming: { delay: { liveDelay: 20 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(20); }) it('should return live delay based on liveDelayFragmentCount if specified in the settings', function () { settings.update({ streaming: { delay: { liveDelayFragmentCount: 5 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); expect(liveDelay).to.equal(10); }) it('should return live delay based on suggestedPresentationDelay', function () { const adapterStub = sinon.stub(adapterMock, 'getSuggestedPresentationDelay').returns(12); - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(12); adapterStub.restore(); }) it('should return live delay based on fragment duration and FRAGMENT_DURATION_FACTOR', function () { - const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); expect(liveDelay).to.equal(8); }) it('should return live delay based on minBufferTime', function () { manifestInfo.minBufferTime = 8; - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(32); }) it('should prefer live delay based on liveDelay if both liveDelay and liveDelayFragmentCount are specified in the settings', function () { settings.update({ streaming: { delay: { liveDelayFragmentCount: 5, liveDelay: 40 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); expect(liveDelay).to.equal(40); }) @@ -168,11 +168,39 @@ describe('PlaybackController', function () { target: 13000 } }] - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(13); }) + it('should apply calculated offsets to liveDelay if ServiceDescription id matches ProducerReferenceTime referenceId', function () { + manifestInfo.serviceDescriptions = [{ + schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019', + latency: { + referenceId: 7, + target: 13000 + } + }] + const timeOffsets = [{id: 5, to: 3}, {id: 7, to: 4}]; + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, timeOffsets, manifestInfo); + + expect(liveDelay).to.equal(9); + }); + + it('should apply time offset based on only present ProducerReferenceTime even if ids do not match', function () { + manifestInfo.serviceDescriptions = [{ + schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019', + latency: { + referenceId: 7, + target: 13000 + } + }] + const timeOffsets = [{id: 5, to: 3}]; + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, timeOffsets, manifestInfo); + + expect(liveDelay).to.equal(10); + }); + it('should ignore live delay based on ServiceDescription if wrong scheme id is specified', function () { manifestInfo.serviceDescriptions = [{ schemeIdUri: 'urn:dvb:dash:somescheme', @@ -180,7 +208,7 @@ describe('PlaybackController', function () { target: 13000 } }] - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.be.NaN }) @@ -193,7 +221,7 @@ describe('PlaybackController', function () { target: 13000 } }] - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(20); }) @@ -206,7 +234,7 @@ describe('PlaybackController', function () { target: 13000 } }] - const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); expect(liveDelay).to.equal(10); }) From 30eb52d90da586d6c41a9db73302a02295095a0f Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Fri, 18 Feb 2022 15:57:06 +0000 Subject: [PATCH 02/18] Calculate and apply time offsets from ProducerReferenceTime when setting liveDelay from ServiceDescription --- src/dash/DashAdapter.js | 23 ++++++ src/dash/constants/DashConstants.js | 4 + src/dash/models/DashManifestModel.js | 42 ++++++++++- src/dash/vo/ProducerReferenceTime.js | 46 ++++++++++++ .../controllers/PlaybackController.js | 40 +++++++++- src/streaming/controllers/StreamController.js | 56 +++++++++++++- test/unit/dash.DashAdapter.js | 17 ++++- test/unit/dash.constants.DashConstants.js | 4 + test/unit/dash.models.DashManifestModel.js | 74 +++++++++++++++++++ ...reaming.controllers.PlaybackControllers.js | 50 ++++++++++--- 10 files changed, 338 insertions(+), 18 deletions(-) create mode 100644 src/dash/vo/ProducerReferenceTime.js diff --git a/src/dash/DashAdapter.js b/src/dash/DashAdapter.js index 67d23f7e92..9786085efb 100644 --- a/src/dash/DashAdapter.js +++ b/src/dash/DashAdapter.js @@ -384,6 +384,28 @@ function DashAdapter() { return realAdaptation; } + /** + * Returns the ProducerReferenceTime as saved in the DashManifestModel if present + * @param {object} streamInfo + * @param {object} mediaInfo + * @returns {object} producerReferenceTime + * @memberOf module:DashAdapter + * @instance + */ + function getProducerReferenceTimes(streamInfo, mediaInfo) { + let id, realAdaptation; + + const selectedVoPeriod = getPeriodForStreamInfo(streamInfo, voPeriods); + id = mediaInfo ? mediaInfo.id : null; + + if (voPeriods.length > 0 && selectedVoPeriod) { + realAdaptation = id ? dashManifestModel.getAdaptationForId(id, voPeriods[0].mpd.manifest, selectedVoPeriod.index) : dashManifestModel.getAdaptationForIndex(mediaInfo ? mediaInfo.index : null, voPeriods[0].mpd.manifest, selectedVoPeriod.index); + } + + if (!realAdaptation) return []; + return dashManifestModel.getProducerReferenceTimesForAdaptation(realAdaptation); + } + /** * Return all EssentialProperties of a Representation * @param {object} representation @@ -1165,6 +1187,7 @@ function DashAdapter() { getAllMediaInfoForType, getAdaptationForType, getRealAdaptation, + getProducerReferenceTimes, getRealPeriodByIndex, getEssentialPropertiesForRepresentation, getVoRepresentations, diff --git a/src/dash/constants/DashConstants.js b/src/dash/constants/DashConstants.js index 6cd85535df..fe01bab6d5 100644 --- a/src/dash/constants/DashConstants.js +++ b/src/dash/constants/DashConstants.js @@ -90,6 +90,7 @@ class DashConstants { this.ESSENTIAL_PROPERTY = 'EssentialProperty'; this.SUPPLEMENTAL_PROPERTY = 'SupplementalProperty'; this.INBAND_EVENT_STREAM = 'InbandEventStream'; + this.PRODUCER_REFERENCE_TIME = 'ProducerReferenceTime'; this.ACCESSIBILITY = 'Accessibility'; this.ROLE = 'Role'; this.RATING = 'Rating'; @@ -98,6 +99,7 @@ class DashConstants { this.LANG = 'lang'; this.VIEWPOINT = 'Viewpoint'; this.ROLE_ASARRAY = 'Role_asArray'; + this.PRODUCERREFERENCETIME_ASARRAY = 'ProducerReferenceTime_asArray'; this.ACCESSIBILITY_ASARRAY = 'Accessibility_asArray'; this.AUDIOCHANNELCONFIGURATION_ASARRAY = 'AudioChannelConfiguration_asArray'; this.CONTENTPROTECTION_ASARRAY = 'ContentProtection_asArray'; @@ -135,6 +137,8 @@ class DashConstants { this.PUBLISH_TIME = 'publishTime'; this.ORIGINAL_PUBLISH_TIME = 'originalPublishTime'; this.ORIGINAL_MPD_ID = 'mpdId'; + this.WALL_CLOCK_TIME = 'wallClockTime'; + this.PRESENTATION_TIME = 'presentationTime'; } constructor () { diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index 6de12eb66f..5fef185629 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -38,6 +38,7 @@ import UTCTiming from '../vo/UTCTiming'; import Event from '../vo/Event'; import BaseURL from '../vo/BaseURL'; import EventStream from '../vo/EventStream'; +import ProducerReferenceTime from '../vo/ProducerReferenceTime'; import ObjectUtils from '../../streaming/utils/ObjectUtils'; import URLUtils from '../../streaming/utils/URLUtils'; import FactoryMaker from '../../core/FactoryMaker'; @@ -162,6 +163,43 @@ function DashManifestModel() { return getIsTypeOf(adaptation, Constants.IMAGE); } + function getProducerReferenceTimesForAdaptation(adaptation) { + const prtArray = adaptation && adaptation.hasOwnProperty(DashConstants.PRODUCERREFERENCETIME_ASARRAY) ? adaptation[DashConstants.PRODUCERREFERENCETIME_ASARRAY] : []; + const prtsForAdaptation = []; + + // Unlikely to have multiple ProducerReferenceTimes. + prtArray.forEach((prt) => { + const entry = new ProducerReferenceTime(); + + if (prt.hasOwnProperty(DashConstants.ID)) { + entry[DashConstants.ID] = prt[DashConstants.ID]; + } else { + // Ignore. Missing mandatory attribute + return; + } + + if (prt.hasOwnProperty(DashConstants.WALL_CLOCK_TIME)) { + entry[DashConstants.WALL_CLOCK_TIME] = prt[DashConstants.WALL_CLOCK_TIME]; + } else { + // Ignore. Missing mandatory attribute + return; + } + + if (prt.hasOwnProperty(DashConstants.PRESENTATION_TIME)) { + entry[DashConstants.PRESENTATION_TIME] = prt[DashConstants.PRESENTATION_TIME]; + } else { + // Ignore. Missing mandatory attribute + return; + } + + // Not intereseted in other attributes for now + // UTC element contained must be same as that in the MPD + prtsForAdaptation.push(entry); + }) + + return prtsForAdaptation; + } + function getLanguageForAdaptation(adaptation) { let lang = ''; @@ -1116,7 +1154,8 @@ function DashManifestModel() { latency = { target: sd[prop].target, max: sd[prop].max, - min: sd[prop].min + min: sd[prop].min, + referenceId: sd[prop].referenceId }; } else if (prop === DashConstants.SERVICE_DESCRIPTION_PLAYBACK_RATE) { playbackRate = { @@ -1170,6 +1209,7 @@ function DashManifestModel() { getIsTypeOf, getIsText, getIsFragmented, + getProducerReferenceTimesForAdaptation, getLanguageForAdaptation, getViewpointForAdaptation, getRolesForAdaptation, diff --git a/src/dash/vo/ProducerReferenceTime.js b/src/dash/vo/ProducerReferenceTime.js new file mode 100644 index 0000000000..8a66353e55 --- /dev/null +++ b/src/dash/vo/ProducerReferenceTime.js @@ -0,0 +1,46 @@ +/** + * The copyright in this software is being made available under the BSD License, + * included below. This software may be subject to other third party and contributor + * rights, including patent rights, and no such rights are granted under this license. + * + * Copyright (c) 2013, Dash Industry Forum. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * * Neither the name of Dash Industry Forum nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +/** + * @class + * @ignore + */ + class ProducerReferenceTime { + constructor() { + this.id = null; + this.inband = false; + this.type = 'encoder'; + this.applicationScheme = null; + this.wallClockTime = null; + this.presentationTime = NaN; + } +} + +export default ProducerReferenceTime; \ No newline at end of file diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index a51fc64ad7..57be0ef219 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -224,7 +224,7 @@ function PlaybackController() { * @returns {number} object * @memberof PlaybackController# */ - function computeAndSetLiveDelay(fragmentDuration, manifestInfo) { + function computeAndSetLiveDelay(fragmentDuration, timeOffsets, manifestInfo) { let delay, ret, startTime; @@ -238,7 +238,7 @@ function PlaybackController() { // Apply live delay from ServiceDescription if (settings.get().streaming.delay.applyServiceDescription && isNaN(settings.get().streaming.delay.liveDelay) && isNaN(settings.get().streaming.delay.liveDelayFragmentCount)) { - _applyServiceDescription(manifestInfo); + _applyServiceDescription(manifestInfo, timeOffsets); } if (mediaPlayerModel.getLiveDelay()) { @@ -272,7 +272,13 @@ function PlaybackController() { return ret; } - function _applyServiceDescription(manifestInfo) { + /** + * Applys service description Latency and PlaybackRate attributes to liveDelay and catchup settings + * @param {Object} manifestInfo + * @param {Array} producerReferenceTimes - All ProducerReferencesTimes in MPD + * @private + */ + function _applyServiceDescription(manifestInfo, timeOffsets) { if (!manifestInfo || !manifestInfo.serviceDescriptions) { return; } @@ -293,7 +299,7 @@ function PlaybackController() { settings.update({ streaming: { delay: { - liveDelay: llsd.latency.target / 1000 + liveDelay: _calculateOffsetLiveDelay(timeOffsets, llsd.latency), } } }); @@ -321,6 +327,32 @@ function PlaybackController() { } } + /** + * Calculates offset to apply to live delay as described in TS 103 285 Clause 10.20.4 + * @param {Array} timeOffsets + * @param {Object} llsdLatency + * @returns {number} + * @private + */ + function _calculateOffsetLiveDelay(timeOffsets, llsdLatency) { + let to = 0; + let offset = timeOffsets.filter(prt => { + return prt.id === llsdLatency.referenceId; + }); + + // If only one ProducerReferenceTime to generate one TO, then use that regardless of matching ids + if (offset.length === 0) { + to = (timeOffsets.length > 0) ? timeOffsets[0].to : 0; + } else { + // If multiple id matches, use the first but this should be invalid + to = offset[0].to || 0; + } + + // TS 103 285 Clause 10.20.4. 3) Subtract calculated offset from Latency@target converted from milliseconds + // liveLatency does not consider ST@availabilityTimeOffset so leave out that step + return ((llsdLatency.target / 1000) - to) + } + function getAvailabilityStartTime() { return availabilityStartTime; } diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index e1329b13b3..bf639875f8 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -49,6 +49,7 @@ import DashJSError from '../vo/DashJSError'; import Errors from '../../core/errors/Errors'; import EventController from './EventController'; import ConformanceViolationConstants from '../constants/ConformanceViolationConstants'; +import DashConstants from '../../dash/constants/DashConstants'; const PLAYBACK_ENDED_TIMER_INTERVAL = 200; const DVR_WAITING_OFFSET = 2; @@ -344,7 +345,8 @@ function StreamController() { if (adapter.getIsDynamic() && streams.length) { const manifestInfo = streamsInfo[0].manifestInfo; const fragmentDuration = _getFragmentDurationForLiveDelayCalculation(streamsInfo, manifestInfo); - playbackController.computeAndSetLiveDelay(fragmentDuration, manifestInfo); + const timeOffsets = _getLiveDelayTimeOffsets(streamsInfo); + playbackController.computeAndSetLiveDelay(fragmentDuration, timeOffsets, manifestInfo); } // Figure out the correct start time and the correct start period @@ -1150,6 +1152,58 @@ function StreamController() { } } + /** + * Calculates an array of time offsets each with matching ProducerReferenceTime id + * @param {Array} streamInfos + * @returns {Array} + * @private + */ + function _getLiveDelayTimeOffsets(streamInfos) { + try { + + let timeOffsets = []; + + if (streamInfos && streamInfos.length === 1) { + const streamInfo = streamInfos[0]; + const mediaTypes = [Constants.VIDEO, Constants.AUDIO, Constants.TEXT]; + const astInSeconds = adapter.getAvailabilityStartTime() / 1000; + + timeOffsets = mediaTypes + .reduce((acc, mediaType) => { + acc = acc.concat(adapter.getAllMediaInfoForType(streamInfo, mediaType)); + return acc; + }, []) + .reduce((acc, mediaInfo) => { + const prts = adapter.getProducerReferenceTimes(streamInfo, mediaInfo); + prts.forEach((prt) => { + const voRepresentations = adapter.getVoRepresentations(mediaInfo); + if (voRepresentations && voRepresentations.length > 0 && voRepresentations[0].adaptation && voRepresentations[0].segmentInfoType === DashConstants.SEGMENT_TEMPLATE) { + const voRep = voRepresentations[0]; + const d = new Date(prt[DashConstants.WALL_CLOCK_TIME]); + const wallClockTime = d.getTime() / 1000; + // TS 103 285 Clause 10.20.4 + // 1) Calculate PRT0 + // i) take the PRT@presentationTime and subtract any ST@presentationTime + // ii) convert this time to seconds by dividing by ST@timescale + // iii) Add this to start time of period that contains PRT. + const prt0 = wallClockTime - (((prt[DashConstants.PRESENTATION_TIME] - voRep[DashConstants.PRESENTATION_TIME_OFFSET]) / voRep[DashConstants.TIMESCALE]) + streamInfo.start); + // 2) Calculate TO between PRT at the start of MPD timeline and the AST + const to = astInSeconds - prt0; + // 3) Not applicable as liveLatency does not consider ST@availabilityTimeOffset + acc.push({id: prt[DashConstants.ID], to}); + } + }); + + return acc; + }, []) + } + + return timeOffsets; + } catch (e) { + return []; + } + }; + /** * Callback handler after the manifest has been updated. Trigger an update in the adapter and filter unsupported stuff. * Finally attempt UTC sync diff --git a/test/unit/dash.DashAdapter.js b/test/unit/dash.DashAdapter.js index 783b177c6f..c451be8479 100644 --- a/test/unit/dash.DashAdapter.js +++ b/test/unit/dash.DashAdapter.js @@ -44,7 +44,7 @@ const manifest_with_ll_service_description = { ServiceDescription: {}, ServiceDescription_asArray: [{ Scope: { schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019' }, - Latency: { target: 3000, max: 5000, min: 2000 }, + Latency: { target: 3000, max: 5000, min: 2000, referenceId: 7 }, PlaybackRate: { max: 1.5, min: 0.5 } }], Period_asArray: [{ @@ -235,6 +235,20 @@ describe('DashAdapter', function () { expect(realAdaptation).to.be.undefined; // jshint ignore:line }); + it('should return empty array when getProducerReferenceTimes is called and streamInfo parameter is null or undefined', () => { + const producerReferenceTimes = dashAdapter.getProducerReferenceTimes(null, voHelper.getDummyMediaInfo()); + + expect(producerReferenceTimes).to.be.instanceOf(Array); // jshint ignore:line + expect(producerReferenceTimes).to.be.empty; // jshint ignore:line + }); + + it('should return empty array when getProducerReferenceTimes is called and mediaInfo parameter is null or undefined', () => { + const producerReferenceTimes = dashAdapter.getProducerReferenceTimes(voHelper.getDummyStreamInfo(), null); + + expect(producerReferenceTimes).to.be.instanceOf(Array); // jshint ignore:line + expect(producerReferenceTimes).to.be.empty; // jshint ignore:line + }); + it('should return empty array when getUTCTimingSources is called and no period is defined', function () { const timingSources = dashAdapter.getUTCTimingSources(); @@ -470,6 +484,7 @@ describe('DashAdapter', function () { expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.target).equals(3000); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.max).equals(5000); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.min).equals(2000); // jshint ignore:line + expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.referenceId).equals(7); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].playbackRate.max).equals(1.5); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].playbackRate.min).equals(0.5); // jshint ignore:line }); diff --git a/test/unit/dash.constants.DashConstants.js b/test/unit/dash.constants.DashConstants.js index cb221fae32..60dd2a6057 100644 --- a/test/unit/dash.constants.DashConstants.js +++ b/test/unit/dash.constants.DashConstants.js @@ -58,6 +58,7 @@ describe('DashConstants', function () { expect(DashConstants.ESSENTIAL_PROPERTY).to.equal('EssentialProperty'); expect(DashConstants.SUPPLEMENTAL_PROPERTY).to.equal('SupplementalProperty'); expect(DashConstants.INBAND_EVENT_STREAM).to.equal('InbandEventStream'); + expect(DashConstants.PRODUCER_REFERENCE_TIME).to.equal('ProducerReferenceTime'); expect(DashConstants.ACCESSIBILITY).to.equal('Accessibility'); expect(DashConstants.ROLE).to.equal('Role'); expect(DashConstants.RATING).to.equal('Rating'); @@ -66,6 +67,7 @@ describe('DashConstants', function () { expect(DashConstants.LANG).to.equal('lang'); expect(DashConstants.VIEWPOINT).to.equal('Viewpoint'); expect(DashConstants.ROLE_ASARRAY).to.equal('Role_asArray'); + expect(DashConstants.PRODUCERREFERENCETIME_ASARRAY).to.equal('ProducerReferenceTime_asArray'); expect(DashConstants.ACCESSIBILITY_ASARRAY).to.equal('Accessibility_asArray'); expect(DashConstants.AUDIOCHANNELCONFIGURATION_ASARRAY).to.equal('AudioChannelConfiguration_asArray'); expect(DashConstants.CONTENTPROTECTION_ASARRAY).to.equal('ContentProtection_asArray'); @@ -92,5 +94,7 @@ describe('DashConstants', function () { expect(DashConstants.DVB_PRIORITY).to.equal('dvb:priority'); expect(DashConstants.DVB_WEIGHT).to.equal('dvb:weight'); expect(DashConstants.SUGGESTED_PRESENTATION_DELAY).to.equal('suggestedPresentationDelay'); + expect(DashConstants.WALL_CLOCK_TIME).to.equal('wallClockTime'); + expect(DashConstants.PRESENTATION_TIME).to.equal('presentationTime'); }); }); diff --git a/test/unit/dash.models.DashManifestModel.js b/test/unit/dash.models.DashManifestModel.js index 029291fcb7..c38e50e553 100644 --- a/test/unit/dash.models.DashManifestModel.js +++ b/test/unit/dash.models.DashManifestModel.js @@ -1107,6 +1107,80 @@ describe('DashManifestModel', function () { }); }); + describe('getProducerReferenceTimesForAdaptation', () => { + it('returns an empty Array when no ProducerReferenceTimes are present on a node', () => { + const node = {}; + + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + expect(obj).to.be.instanceOf(Array); // jshint ignore:line + expect(obj).to.be.empty; // jshint ignore:line + }); + + it('returns an empty Array where a single ProducerReferenceTime element on a node has missing mandatory attributes', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 4, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:00Z' + // missing presentationTime + } + ] + }; + + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + expect(obj).to.be.instanceOf(Array); // jshint ignore:line + expect(obj).to.be.empty; // jshint ignore:line + }); + + it('returns an Array of ProducerReferenceTime elements with mandatory attributes', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 4, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:04Z', + [DashConstants.PRESENTATION_TIME]: 0 + }, + { + [DashConstants.ID]: 5, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:05Z', + [DashConstants.PRESENTATION_TIME]: 1 + } + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + /* jshint ignore:start */ + expect(obj).to.be.instanceOf(Array); + expect(obj).to.have.lengthOf(2); + expect(obj[0][DashConstants.ID]).to.equal(4); + expect(obj[0][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:04Z'); + expect(obj[0][DashConstants.PRESENTATION_TIME]).to.equal(0); + expect(obj[1][DashConstants.ID]).to.equal(5); + expect(obj[1][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:05Z'); + expect(obj[1][DashConstants.PRESENTATION_TIME]).to.equal(1); + /* jshint ignore:end */ + }); + }); + + it('returns ProducerReferenceTimes with correct default attribute values', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 4, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:04Z', + [DashConstants.PRESENTATION_TIME]: 0 + } + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + expect(obj).to.be.instanceOf(Array); // jshint ignore:line + expect(obj).to.have.lengthOf(1); // jshint ignore:line + expect(obj[0].type).to.equal('encoder'); // jshint ignore:line + }); + describe('getSelectionPriority', () => { it('should return 1 when adaptation is not defined', () => { diff --git a/test/unit/streaming.controllers.PlaybackControllers.js b/test/unit/streaming.controllers.PlaybackControllers.js index a757c19752..3d3a65b2eb 100644 --- a/test/unit/streaming.controllers.PlaybackControllers.js +++ b/test/unit/streaming.controllers.PlaybackControllers.js @@ -114,49 +114,49 @@ describe('PlaybackController', function () { }) it('should return NaN if no values specified', function () { - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.be.NaN; }) it('should return live delay if specified in the settings', function () { settings.update({ streaming: { delay: { liveDelay: 20 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(20); }) it('should return live delay based on liveDelayFragmentCount if specified in the settings', function () { settings.update({ streaming: { delay: { liveDelayFragmentCount: 5 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); expect(liveDelay).to.equal(10); }) it('should return live delay based on suggestedPresentationDelay', function () { const adapterStub = sinon.stub(adapterMock, 'getSuggestedPresentationDelay').returns(12); - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(12); adapterStub.restore(); }) it('should return live delay based on fragment duration and FRAGMENT_DURATION_FACTOR', function () { - const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); expect(liveDelay).to.equal(8); }) it('should return live delay based on minBufferTime', function () { manifestInfo.minBufferTime = 8; - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(32); }) it('should prefer live delay based on liveDelay if both liveDelay and liveDelayFragmentCount are specified in the settings', function () { settings.update({ streaming: { delay: { liveDelayFragmentCount: 5, liveDelay: 40 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); expect(liveDelay).to.equal(40); }) @@ -168,11 +168,39 @@ describe('PlaybackController', function () { target: 13000 } }] - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(13); }) + it('should apply calculated offsets to liveDelay if ServiceDescription id matches ProducerReferenceTime referenceId', function () { + manifestInfo.serviceDescriptions = [{ + schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019', + latency: { + referenceId: 7, + target: 13000 + } + }] + const timeOffsets = [{id: 5, to: 3}, {id: 7, to: 4}]; + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, timeOffsets, manifestInfo); + + expect(liveDelay).to.equal(9); + }); + + it('should apply time offset based on only present ProducerReferenceTime even if ids do not match', function () { + manifestInfo.serviceDescriptions = [{ + schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019', + latency: { + referenceId: 7, + target: 13000 + } + }] + const timeOffsets = [{id: 5, to: 3}]; + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, timeOffsets, manifestInfo); + + expect(liveDelay).to.equal(10); + }); + it('should ignore live delay based on ServiceDescription if wrong scheme id is specified', function () { manifestInfo.serviceDescriptions = [{ schemeIdUri: 'urn:dvb:dash:somescheme', @@ -180,7 +208,7 @@ describe('PlaybackController', function () { target: 13000 } }] - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.be.NaN }) @@ -193,7 +221,7 @@ describe('PlaybackController', function () { target: 13000 } }] - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(20); }) @@ -206,7 +234,7 @@ describe('PlaybackController', function () { target: 13000 } }] - const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); expect(liveDelay).to.equal(10); }) From 4242a80f5412f80ae00784df7aa121469dacab18 Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Mon, 21 Feb 2022 15:14:56 +0000 Subject: [PATCH 03/18] Clearer variables and function names --- src/dash/DashAdapter.js | 4 ++-- src/streaming/controllers/PlaybackController.js | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/dash/DashAdapter.js b/src/dash/DashAdapter.js index 9786085efb..e2cf957e23 100644 --- a/src/dash/DashAdapter.js +++ b/src/dash/DashAdapter.js @@ -385,10 +385,10 @@ function DashAdapter() { } /** - * Returns the ProducerReferenceTime as saved in the DashManifestModel if present + * Returns the ProducerReferenceTimes as saved in the DashManifestModel if present * @param {object} streamInfo * @param {object} mediaInfo - * @returns {object} producerReferenceTime + * @returns {object} producerReferenceTimes * @memberOf module:DashAdapter * @instance */ diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index 57be0ef219..3ff0233514 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -275,10 +275,10 @@ function PlaybackController() { /** * Applys service description Latency and PlaybackRate attributes to liveDelay and catchup settings * @param {Object} manifestInfo - * @param {Array} producerReferenceTimes - All ProducerReferencesTimes in MPD + * @param {Array} prftTimeOffsets - Time offests calculated from ProducerReferenceTime elements * @private */ - function _applyServiceDescription(manifestInfo, timeOffsets) { + function _applyServiceDescription(manifestInfo, prftTimeOffsets) { if (!manifestInfo || !manifestInfo.serviceDescriptions) { return; } @@ -299,7 +299,7 @@ function PlaybackController() { settings.update({ streaming: { delay: { - liveDelay: _calculateOffsetLiveDelay(timeOffsets, llsd.latency), + liveDelay: _calculateDelayFromServiceDescription(prftTimeOffsets, llsd.latency), } } }); @@ -329,20 +329,20 @@ function PlaybackController() { /** * Calculates offset to apply to live delay as described in TS 103 285 Clause 10.20.4 - * @param {Array} timeOffsets + * @param {Array} prftTimeOffsets * @param {Object} llsdLatency * @returns {number} * @private */ - function _calculateOffsetLiveDelay(timeOffsets, llsdLatency) { + function _calculateDelayFromServiceDescription(prftTimeOffsets, llsdLatency) { let to = 0; - let offset = timeOffsets.filter(prt => { + let offset = prftTimeOffsets.filter(prt => { return prt.id === llsdLatency.referenceId; }); // If only one ProducerReferenceTime to generate one TO, then use that regardless of matching ids if (offset.length === 0) { - to = (timeOffsets.length > 0) ? timeOffsets[0].to : 0; + to = (prftTimeOffsets.length > 0) ? prftTimeOffsets[0].to : 0; } else { // If multiple id matches, use the first but this should be invalid to = offset[0].to || 0; From 8ef6da318d0e12bce247343d2b94f104a0f1dfd7 Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Tue, 22 Feb 2022 13:23:00 +0000 Subject: [PATCH 04/18] Add collection of PRFTs from Representations --- src/dash/constants/DashConstants.js | 1 + src/dash/models/DashManifestModel.js | 10 ++++ test/unit/dash.constants.DashConstants.js | 1 + test/unit/dash.models.DashManifestModel.js | 69 +++++++++++++++++----- 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/dash/constants/DashConstants.js b/src/dash/constants/DashConstants.js index fe01bab6d5..3b34c7fa53 100644 --- a/src/dash/constants/DashConstants.js +++ b/src/dash/constants/DashConstants.js @@ -99,6 +99,7 @@ class DashConstants { this.LANG = 'lang'; this.VIEWPOINT = 'Viewpoint'; this.ROLE_ASARRAY = 'Role_asArray'; + this.REPRESENTATION_ASARRAY = 'Representation_asArray'; this.PRODUCERREFERENCETIME_ASARRAY = 'ProducerReferenceTime_asArray'; this.ACCESSIBILITY_ASARRAY = 'Accessibility_asArray'; this.AUDIOCHANNELCONFIGURATION_ASARRAY = 'AudioChannelConfiguration_asArray'; diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index 5fef185629..df1a913e7c 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -165,6 +165,16 @@ function DashManifestModel() { function getProducerReferenceTimesForAdaptation(adaptation) { const prtArray = adaptation && adaptation.hasOwnProperty(DashConstants.PRODUCERREFERENCETIME_ASARRAY) ? adaptation[DashConstants.PRODUCERREFERENCETIME_ASARRAY] : []; + + // ProducerReferenceTime elements can also be contained in Representations + const representationsArray = adaptation && adaptation.hasOwnProperty(DashConstants.REPRESENTATION_ASARRAY) ? adaptation[DashConstants.REPRESENTATION_ASARRAY] : []; + + representationsArray.forEach((rep) => { + if (rep.hasOwnProperty(DashConstants.PRODUCERREFERENCETIME_ASARRAY)) { + prtArray.push(...rep[DashConstants.PRODUCERREFERENCETIME_ASARRAY]); + } + }); + const prtsForAdaptation = []; // Unlikely to have multiple ProducerReferenceTimes. diff --git a/test/unit/dash.constants.DashConstants.js b/test/unit/dash.constants.DashConstants.js index 60dd2a6057..0c6be60bbf 100644 --- a/test/unit/dash.constants.DashConstants.js +++ b/test/unit/dash.constants.DashConstants.js @@ -67,6 +67,7 @@ describe('DashConstants', function () { expect(DashConstants.LANG).to.equal('lang'); expect(DashConstants.VIEWPOINT).to.equal('Viewpoint'); expect(DashConstants.ROLE_ASARRAY).to.equal('Role_asArray'); + expect(DashConstants.REPRESENTATION_ASARRAY).to.equal('Representation_asArray'); expect(DashConstants.PRODUCERREFERENCETIME_ASARRAY).to.equal('ProducerReferenceTime_asArray'); expect(DashConstants.ACCESSIBILITY_ASARRAY).to.equal('Accessibility_asArray'); expect(DashConstants.AUDIOCHANNELCONFIGURATION_ASARRAY).to.equal('AudioChannelConfiguration_asArray'); diff --git a/test/unit/dash.models.DashManifestModel.js b/test/unit/dash.models.DashManifestModel.js index c38e50e553..d60892c89b 100644 --- a/test/unit/dash.models.DashManifestModel.js +++ b/test/unit/dash.models.DashManifestModel.js @@ -1162,25 +1162,64 @@ describe('DashManifestModel', function () { expect(obj[1][DashConstants.PRESENTATION_TIME]).to.equal(1); /* jshint ignore:end */ }); - }); - it('returns ProducerReferenceTimes with correct default attribute values', () => { - const node = { - [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ - { - [DashConstants.ID]: 4, - [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:04Z', - [DashConstants.PRESENTATION_TIME]: 0 - } - ] - }; - const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + it('returns ProducerReferenceTimes with correct default attribute values', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 4, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:04Z', + [DashConstants.PRESENTATION_TIME]: 0 + } + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + expect(obj).to.be.instanceOf(Array); // jshint ignore:line + expect(obj).to.have.lengthOf(1); // jshint ignore:line + expect(obj[0].type).to.equal('encoder'); // jshint ignore:line + }); + + it('returns ProducerReferenceTimes within representations', () => { + const node = { + [DashConstants.REPRESENTATION_ASARRAY] : [ + { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 1, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:01Z', + [DashConstants.PRESENTATION_TIME]: 0 + } + ] + }, + { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 2, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:02Z', + [DashConstants.PRESENTATION_TIME]: 1 + } + ] + }, + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + /* jshint ignore:start */ + expect(obj).to.be.instanceOf(Array); + expect(obj).to.have.lengthOf(2); + expect(obj[0][DashConstants.ID]).to.equal(1); + expect(obj[0][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:01Z'); + expect(obj[0][DashConstants.PRESENTATION_TIME]).to.equal(0); + expect(obj[1][DashConstants.ID]).to.equal(2); + expect(obj[1][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:02Z'); + expect(obj[1][DashConstants.PRESENTATION_TIME]).to.equal(1); + /* jshint ignore:end */ + + }); - expect(obj).to.be.instanceOf(Array); // jshint ignore:line - expect(obj).to.have.lengthOf(1); // jshint ignore:line - expect(obj[0].type).to.equal('encoder'); // jshint ignore:line }); + describe('getSelectionPriority', () => { it('should return 1 when adaptation is not defined', () => { From 9b345ff2296414e10bbfab794b803873abe6289c Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Fri, 18 Feb 2022 15:57:06 +0000 Subject: [PATCH 05/18] Calculate and apply time offsets from ProducerReferenceTime when setting liveDelay from ServiceDescription --- src/dash/DashAdapter.js | 23 ++++++ src/dash/constants/DashConstants.js | 4 + src/dash/models/DashManifestModel.js | 42 ++++++++++- src/dash/vo/ProducerReferenceTime.js | 46 ++++++++++++ .../controllers/PlaybackController.js | 40 +++++++++- src/streaming/controllers/StreamController.js | 56 +++++++++++++- test/unit/dash.DashAdapter.js | 17 ++++- test/unit/dash.constants.DashConstants.js | 4 + test/unit/dash.models.DashManifestModel.js | 74 +++++++++++++++++++ ...reaming.controllers.PlaybackControllers.js | 50 ++++++++++--- 10 files changed, 338 insertions(+), 18 deletions(-) create mode 100644 src/dash/vo/ProducerReferenceTime.js diff --git a/src/dash/DashAdapter.js b/src/dash/DashAdapter.js index 67d23f7e92..9786085efb 100644 --- a/src/dash/DashAdapter.js +++ b/src/dash/DashAdapter.js @@ -384,6 +384,28 @@ function DashAdapter() { return realAdaptation; } + /** + * Returns the ProducerReferenceTime as saved in the DashManifestModel if present + * @param {object} streamInfo + * @param {object} mediaInfo + * @returns {object} producerReferenceTime + * @memberOf module:DashAdapter + * @instance + */ + function getProducerReferenceTimes(streamInfo, mediaInfo) { + let id, realAdaptation; + + const selectedVoPeriod = getPeriodForStreamInfo(streamInfo, voPeriods); + id = mediaInfo ? mediaInfo.id : null; + + if (voPeriods.length > 0 && selectedVoPeriod) { + realAdaptation = id ? dashManifestModel.getAdaptationForId(id, voPeriods[0].mpd.manifest, selectedVoPeriod.index) : dashManifestModel.getAdaptationForIndex(mediaInfo ? mediaInfo.index : null, voPeriods[0].mpd.manifest, selectedVoPeriod.index); + } + + if (!realAdaptation) return []; + return dashManifestModel.getProducerReferenceTimesForAdaptation(realAdaptation); + } + /** * Return all EssentialProperties of a Representation * @param {object} representation @@ -1165,6 +1187,7 @@ function DashAdapter() { getAllMediaInfoForType, getAdaptationForType, getRealAdaptation, + getProducerReferenceTimes, getRealPeriodByIndex, getEssentialPropertiesForRepresentation, getVoRepresentations, diff --git a/src/dash/constants/DashConstants.js b/src/dash/constants/DashConstants.js index 6cd85535df..fe01bab6d5 100644 --- a/src/dash/constants/DashConstants.js +++ b/src/dash/constants/DashConstants.js @@ -90,6 +90,7 @@ class DashConstants { this.ESSENTIAL_PROPERTY = 'EssentialProperty'; this.SUPPLEMENTAL_PROPERTY = 'SupplementalProperty'; this.INBAND_EVENT_STREAM = 'InbandEventStream'; + this.PRODUCER_REFERENCE_TIME = 'ProducerReferenceTime'; this.ACCESSIBILITY = 'Accessibility'; this.ROLE = 'Role'; this.RATING = 'Rating'; @@ -98,6 +99,7 @@ class DashConstants { this.LANG = 'lang'; this.VIEWPOINT = 'Viewpoint'; this.ROLE_ASARRAY = 'Role_asArray'; + this.PRODUCERREFERENCETIME_ASARRAY = 'ProducerReferenceTime_asArray'; this.ACCESSIBILITY_ASARRAY = 'Accessibility_asArray'; this.AUDIOCHANNELCONFIGURATION_ASARRAY = 'AudioChannelConfiguration_asArray'; this.CONTENTPROTECTION_ASARRAY = 'ContentProtection_asArray'; @@ -135,6 +137,8 @@ class DashConstants { this.PUBLISH_TIME = 'publishTime'; this.ORIGINAL_PUBLISH_TIME = 'originalPublishTime'; this.ORIGINAL_MPD_ID = 'mpdId'; + this.WALL_CLOCK_TIME = 'wallClockTime'; + this.PRESENTATION_TIME = 'presentationTime'; } constructor () { diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index 6de12eb66f..5fef185629 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -38,6 +38,7 @@ import UTCTiming from '../vo/UTCTiming'; import Event from '../vo/Event'; import BaseURL from '../vo/BaseURL'; import EventStream from '../vo/EventStream'; +import ProducerReferenceTime from '../vo/ProducerReferenceTime'; import ObjectUtils from '../../streaming/utils/ObjectUtils'; import URLUtils from '../../streaming/utils/URLUtils'; import FactoryMaker from '../../core/FactoryMaker'; @@ -162,6 +163,43 @@ function DashManifestModel() { return getIsTypeOf(adaptation, Constants.IMAGE); } + function getProducerReferenceTimesForAdaptation(adaptation) { + const prtArray = adaptation && adaptation.hasOwnProperty(DashConstants.PRODUCERREFERENCETIME_ASARRAY) ? adaptation[DashConstants.PRODUCERREFERENCETIME_ASARRAY] : []; + const prtsForAdaptation = []; + + // Unlikely to have multiple ProducerReferenceTimes. + prtArray.forEach((prt) => { + const entry = new ProducerReferenceTime(); + + if (prt.hasOwnProperty(DashConstants.ID)) { + entry[DashConstants.ID] = prt[DashConstants.ID]; + } else { + // Ignore. Missing mandatory attribute + return; + } + + if (prt.hasOwnProperty(DashConstants.WALL_CLOCK_TIME)) { + entry[DashConstants.WALL_CLOCK_TIME] = prt[DashConstants.WALL_CLOCK_TIME]; + } else { + // Ignore. Missing mandatory attribute + return; + } + + if (prt.hasOwnProperty(DashConstants.PRESENTATION_TIME)) { + entry[DashConstants.PRESENTATION_TIME] = prt[DashConstants.PRESENTATION_TIME]; + } else { + // Ignore. Missing mandatory attribute + return; + } + + // Not intereseted in other attributes for now + // UTC element contained must be same as that in the MPD + prtsForAdaptation.push(entry); + }) + + return prtsForAdaptation; + } + function getLanguageForAdaptation(adaptation) { let lang = ''; @@ -1116,7 +1154,8 @@ function DashManifestModel() { latency = { target: sd[prop].target, max: sd[prop].max, - min: sd[prop].min + min: sd[prop].min, + referenceId: sd[prop].referenceId }; } else if (prop === DashConstants.SERVICE_DESCRIPTION_PLAYBACK_RATE) { playbackRate = { @@ -1170,6 +1209,7 @@ function DashManifestModel() { getIsTypeOf, getIsText, getIsFragmented, + getProducerReferenceTimesForAdaptation, getLanguageForAdaptation, getViewpointForAdaptation, getRolesForAdaptation, diff --git a/src/dash/vo/ProducerReferenceTime.js b/src/dash/vo/ProducerReferenceTime.js new file mode 100644 index 0000000000..8a66353e55 --- /dev/null +++ b/src/dash/vo/ProducerReferenceTime.js @@ -0,0 +1,46 @@ +/** + * The copyright in this software is being made available under the BSD License, + * included below. This software may be subject to other third party and contributor + * rights, including patent rights, and no such rights are granted under this license. + * + * Copyright (c) 2013, Dash Industry Forum. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * * Neither the name of Dash Industry Forum nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +/** + * @class + * @ignore + */ + class ProducerReferenceTime { + constructor() { + this.id = null; + this.inband = false; + this.type = 'encoder'; + this.applicationScheme = null; + this.wallClockTime = null; + this.presentationTime = NaN; + } +} + +export default ProducerReferenceTime; \ No newline at end of file diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index a51fc64ad7..57be0ef219 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -224,7 +224,7 @@ function PlaybackController() { * @returns {number} object * @memberof PlaybackController# */ - function computeAndSetLiveDelay(fragmentDuration, manifestInfo) { + function computeAndSetLiveDelay(fragmentDuration, timeOffsets, manifestInfo) { let delay, ret, startTime; @@ -238,7 +238,7 @@ function PlaybackController() { // Apply live delay from ServiceDescription if (settings.get().streaming.delay.applyServiceDescription && isNaN(settings.get().streaming.delay.liveDelay) && isNaN(settings.get().streaming.delay.liveDelayFragmentCount)) { - _applyServiceDescription(manifestInfo); + _applyServiceDescription(manifestInfo, timeOffsets); } if (mediaPlayerModel.getLiveDelay()) { @@ -272,7 +272,13 @@ function PlaybackController() { return ret; } - function _applyServiceDescription(manifestInfo) { + /** + * Applys service description Latency and PlaybackRate attributes to liveDelay and catchup settings + * @param {Object} manifestInfo + * @param {Array} producerReferenceTimes - All ProducerReferencesTimes in MPD + * @private + */ + function _applyServiceDescription(manifestInfo, timeOffsets) { if (!manifestInfo || !manifestInfo.serviceDescriptions) { return; } @@ -293,7 +299,7 @@ function PlaybackController() { settings.update({ streaming: { delay: { - liveDelay: llsd.latency.target / 1000 + liveDelay: _calculateOffsetLiveDelay(timeOffsets, llsd.latency), } } }); @@ -321,6 +327,32 @@ function PlaybackController() { } } + /** + * Calculates offset to apply to live delay as described in TS 103 285 Clause 10.20.4 + * @param {Array} timeOffsets + * @param {Object} llsdLatency + * @returns {number} + * @private + */ + function _calculateOffsetLiveDelay(timeOffsets, llsdLatency) { + let to = 0; + let offset = timeOffsets.filter(prt => { + return prt.id === llsdLatency.referenceId; + }); + + // If only one ProducerReferenceTime to generate one TO, then use that regardless of matching ids + if (offset.length === 0) { + to = (timeOffsets.length > 0) ? timeOffsets[0].to : 0; + } else { + // If multiple id matches, use the first but this should be invalid + to = offset[0].to || 0; + } + + // TS 103 285 Clause 10.20.4. 3) Subtract calculated offset from Latency@target converted from milliseconds + // liveLatency does not consider ST@availabilityTimeOffset so leave out that step + return ((llsdLatency.target / 1000) - to) + } + function getAvailabilityStartTime() { return availabilityStartTime; } diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index e1329b13b3..bf639875f8 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -49,6 +49,7 @@ import DashJSError from '../vo/DashJSError'; import Errors from '../../core/errors/Errors'; import EventController from './EventController'; import ConformanceViolationConstants from '../constants/ConformanceViolationConstants'; +import DashConstants from '../../dash/constants/DashConstants'; const PLAYBACK_ENDED_TIMER_INTERVAL = 200; const DVR_WAITING_OFFSET = 2; @@ -344,7 +345,8 @@ function StreamController() { if (adapter.getIsDynamic() && streams.length) { const manifestInfo = streamsInfo[0].manifestInfo; const fragmentDuration = _getFragmentDurationForLiveDelayCalculation(streamsInfo, manifestInfo); - playbackController.computeAndSetLiveDelay(fragmentDuration, manifestInfo); + const timeOffsets = _getLiveDelayTimeOffsets(streamsInfo); + playbackController.computeAndSetLiveDelay(fragmentDuration, timeOffsets, manifestInfo); } // Figure out the correct start time and the correct start period @@ -1150,6 +1152,58 @@ function StreamController() { } } + /** + * Calculates an array of time offsets each with matching ProducerReferenceTime id + * @param {Array} streamInfos + * @returns {Array} + * @private + */ + function _getLiveDelayTimeOffsets(streamInfos) { + try { + + let timeOffsets = []; + + if (streamInfos && streamInfos.length === 1) { + const streamInfo = streamInfos[0]; + const mediaTypes = [Constants.VIDEO, Constants.AUDIO, Constants.TEXT]; + const astInSeconds = adapter.getAvailabilityStartTime() / 1000; + + timeOffsets = mediaTypes + .reduce((acc, mediaType) => { + acc = acc.concat(adapter.getAllMediaInfoForType(streamInfo, mediaType)); + return acc; + }, []) + .reduce((acc, mediaInfo) => { + const prts = adapter.getProducerReferenceTimes(streamInfo, mediaInfo); + prts.forEach((prt) => { + const voRepresentations = adapter.getVoRepresentations(mediaInfo); + if (voRepresentations && voRepresentations.length > 0 && voRepresentations[0].adaptation && voRepresentations[0].segmentInfoType === DashConstants.SEGMENT_TEMPLATE) { + const voRep = voRepresentations[0]; + const d = new Date(prt[DashConstants.WALL_CLOCK_TIME]); + const wallClockTime = d.getTime() / 1000; + // TS 103 285 Clause 10.20.4 + // 1) Calculate PRT0 + // i) take the PRT@presentationTime and subtract any ST@presentationTime + // ii) convert this time to seconds by dividing by ST@timescale + // iii) Add this to start time of period that contains PRT. + const prt0 = wallClockTime - (((prt[DashConstants.PRESENTATION_TIME] - voRep[DashConstants.PRESENTATION_TIME_OFFSET]) / voRep[DashConstants.TIMESCALE]) + streamInfo.start); + // 2) Calculate TO between PRT at the start of MPD timeline and the AST + const to = astInSeconds - prt0; + // 3) Not applicable as liveLatency does not consider ST@availabilityTimeOffset + acc.push({id: prt[DashConstants.ID], to}); + } + }); + + return acc; + }, []) + } + + return timeOffsets; + } catch (e) { + return []; + } + }; + /** * Callback handler after the manifest has been updated. Trigger an update in the adapter and filter unsupported stuff. * Finally attempt UTC sync diff --git a/test/unit/dash.DashAdapter.js b/test/unit/dash.DashAdapter.js index 783b177c6f..c451be8479 100644 --- a/test/unit/dash.DashAdapter.js +++ b/test/unit/dash.DashAdapter.js @@ -44,7 +44,7 @@ const manifest_with_ll_service_description = { ServiceDescription: {}, ServiceDescription_asArray: [{ Scope: { schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019' }, - Latency: { target: 3000, max: 5000, min: 2000 }, + Latency: { target: 3000, max: 5000, min: 2000, referenceId: 7 }, PlaybackRate: { max: 1.5, min: 0.5 } }], Period_asArray: [{ @@ -235,6 +235,20 @@ describe('DashAdapter', function () { expect(realAdaptation).to.be.undefined; // jshint ignore:line }); + it('should return empty array when getProducerReferenceTimes is called and streamInfo parameter is null or undefined', () => { + const producerReferenceTimes = dashAdapter.getProducerReferenceTimes(null, voHelper.getDummyMediaInfo()); + + expect(producerReferenceTimes).to.be.instanceOf(Array); // jshint ignore:line + expect(producerReferenceTimes).to.be.empty; // jshint ignore:line + }); + + it('should return empty array when getProducerReferenceTimes is called and mediaInfo parameter is null or undefined', () => { + const producerReferenceTimes = dashAdapter.getProducerReferenceTimes(voHelper.getDummyStreamInfo(), null); + + expect(producerReferenceTimes).to.be.instanceOf(Array); // jshint ignore:line + expect(producerReferenceTimes).to.be.empty; // jshint ignore:line + }); + it('should return empty array when getUTCTimingSources is called and no period is defined', function () { const timingSources = dashAdapter.getUTCTimingSources(); @@ -470,6 +484,7 @@ describe('DashAdapter', function () { expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.target).equals(3000); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.max).equals(5000); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.min).equals(2000); // jshint ignore:line + expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.referenceId).equals(7); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].playbackRate.max).equals(1.5); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].playbackRate.min).equals(0.5); // jshint ignore:line }); diff --git a/test/unit/dash.constants.DashConstants.js b/test/unit/dash.constants.DashConstants.js index cb221fae32..60dd2a6057 100644 --- a/test/unit/dash.constants.DashConstants.js +++ b/test/unit/dash.constants.DashConstants.js @@ -58,6 +58,7 @@ describe('DashConstants', function () { expect(DashConstants.ESSENTIAL_PROPERTY).to.equal('EssentialProperty'); expect(DashConstants.SUPPLEMENTAL_PROPERTY).to.equal('SupplementalProperty'); expect(DashConstants.INBAND_EVENT_STREAM).to.equal('InbandEventStream'); + expect(DashConstants.PRODUCER_REFERENCE_TIME).to.equal('ProducerReferenceTime'); expect(DashConstants.ACCESSIBILITY).to.equal('Accessibility'); expect(DashConstants.ROLE).to.equal('Role'); expect(DashConstants.RATING).to.equal('Rating'); @@ -66,6 +67,7 @@ describe('DashConstants', function () { expect(DashConstants.LANG).to.equal('lang'); expect(DashConstants.VIEWPOINT).to.equal('Viewpoint'); expect(DashConstants.ROLE_ASARRAY).to.equal('Role_asArray'); + expect(DashConstants.PRODUCERREFERENCETIME_ASARRAY).to.equal('ProducerReferenceTime_asArray'); expect(DashConstants.ACCESSIBILITY_ASARRAY).to.equal('Accessibility_asArray'); expect(DashConstants.AUDIOCHANNELCONFIGURATION_ASARRAY).to.equal('AudioChannelConfiguration_asArray'); expect(DashConstants.CONTENTPROTECTION_ASARRAY).to.equal('ContentProtection_asArray'); @@ -92,5 +94,7 @@ describe('DashConstants', function () { expect(DashConstants.DVB_PRIORITY).to.equal('dvb:priority'); expect(DashConstants.DVB_WEIGHT).to.equal('dvb:weight'); expect(DashConstants.SUGGESTED_PRESENTATION_DELAY).to.equal('suggestedPresentationDelay'); + expect(DashConstants.WALL_CLOCK_TIME).to.equal('wallClockTime'); + expect(DashConstants.PRESENTATION_TIME).to.equal('presentationTime'); }); }); diff --git a/test/unit/dash.models.DashManifestModel.js b/test/unit/dash.models.DashManifestModel.js index 029291fcb7..c38e50e553 100644 --- a/test/unit/dash.models.DashManifestModel.js +++ b/test/unit/dash.models.DashManifestModel.js @@ -1107,6 +1107,80 @@ describe('DashManifestModel', function () { }); }); + describe('getProducerReferenceTimesForAdaptation', () => { + it('returns an empty Array when no ProducerReferenceTimes are present on a node', () => { + const node = {}; + + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + expect(obj).to.be.instanceOf(Array); // jshint ignore:line + expect(obj).to.be.empty; // jshint ignore:line + }); + + it('returns an empty Array where a single ProducerReferenceTime element on a node has missing mandatory attributes', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 4, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:00Z' + // missing presentationTime + } + ] + }; + + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + expect(obj).to.be.instanceOf(Array); // jshint ignore:line + expect(obj).to.be.empty; // jshint ignore:line + }); + + it('returns an Array of ProducerReferenceTime elements with mandatory attributes', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 4, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:04Z', + [DashConstants.PRESENTATION_TIME]: 0 + }, + { + [DashConstants.ID]: 5, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:05Z', + [DashConstants.PRESENTATION_TIME]: 1 + } + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + /* jshint ignore:start */ + expect(obj).to.be.instanceOf(Array); + expect(obj).to.have.lengthOf(2); + expect(obj[0][DashConstants.ID]).to.equal(4); + expect(obj[0][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:04Z'); + expect(obj[0][DashConstants.PRESENTATION_TIME]).to.equal(0); + expect(obj[1][DashConstants.ID]).to.equal(5); + expect(obj[1][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:05Z'); + expect(obj[1][DashConstants.PRESENTATION_TIME]).to.equal(1); + /* jshint ignore:end */ + }); + }); + + it('returns ProducerReferenceTimes with correct default attribute values', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 4, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:04Z', + [DashConstants.PRESENTATION_TIME]: 0 + } + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + expect(obj).to.be.instanceOf(Array); // jshint ignore:line + expect(obj).to.have.lengthOf(1); // jshint ignore:line + expect(obj[0].type).to.equal('encoder'); // jshint ignore:line + }); + describe('getSelectionPriority', () => { it('should return 1 when adaptation is not defined', () => { diff --git a/test/unit/streaming.controllers.PlaybackControllers.js b/test/unit/streaming.controllers.PlaybackControllers.js index a757c19752..3d3a65b2eb 100644 --- a/test/unit/streaming.controllers.PlaybackControllers.js +++ b/test/unit/streaming.controllers.PlaybackControllers.js @@ -114,49 +114,49 @@ describe('PlaybackController', function () { }) it('should return NaN if no values specified', function () { - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.be.NaN; }) it('should return live delay if specified in the settings', function () { settings.update({ streaming: { delay: { liveDelay: 20 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(20); }) it('should return live delay based on liveDelayFragmentCount if specified in the settings', function () { settings.update({ streaming: { delay: { liveDelayFragmentCount: 5 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); expect(liveDelay).to.equal(10); }) it('should return live delay based on suggestedPresentationDelay', function () { const adapterStub = sinon.stub(adapterMock, 'getSuggestedPresentationDelay').returns(12); - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(12); adapterStub.restore(); }) it('should return live delay based on fragment duration and FRAGMENT_DURATION_FACTOR', function () { - const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); expect(liveDelay).to.equal(8); }) it('should return live delay based on minBufferTime', function () { manifestInfo.minBufferTime = 8; - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(32); }) it('should prefer live delay based on liveDelay if both liveDelay and liveDelayFragmentCount are specified in the settings', function () { settings.update({ streaming: { delay: { liveDelayFragmentCount: 5, liveDelay: 40 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); expect(liveDelay).to.equal(40); }) @@ -168,11 +168,39 @@ describe('PlaybackController', function () { target: 13000 } }] - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(13); }) + it('should apply calculated offsets to liveDelay if ServiceDescription id matches ProducerReferenceTime referenceId', function () { + manifestInfo.serviceDescriptions = [{ + schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019', + latency: { + referenceId: 7, + target: 13000 + } + }] + const timeOffsets = [{id: 5, to: 3}, {id: 7, to: 4}]; + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, timeOffsets, manifestInfo); + + expect(liveDelay).to.equal(9); + }); + + it('should apply time offset based on only present ProducerReferenceTime even if ids do not match', function () { + manifestInfo.serviceDescriptions = [{ + schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019', + latency: { + referenceId: 7, + target: 13000 + } + }] + const timeOffsets = [{id: 5, to: 3}]; + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, timeOffsets, manifestInfo); + + expect(liveDelay).to.equal(10); + }); + it('should ignore live delay based on ServiceDescription if wrong scheme id is specified', function () { manifestInfo.serviceDescriptions = [{ schemeIdUri: 'urn:dvb:dash:somescheme', @@ -180,7 +208,7 @@ describe('PlaybackController', function () { target: 13000 } }] - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.be.NaN }) @@ -193,7 +221,7 @@ describe('PlaybackController', function () { target: 13000 } }] - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(20); }) @@ -206,7 +234,7 @@ describe('PlaybackController', function () { target: 13000 } }] - const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); expect(liveDelay).to.equal(10); }) From ef30e51c94b1d77c6750254ec99f328eb6aa3da6 Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Mon, 21 Feb 2022 15:14:56 +0000 Subject: [PATCH 06/18] Clearer variables and function names --- src/dash/DashAdapter.js | 4 ++-- src/streaming/controllers/PlaybackController.js | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/dash/DashAdapter.js b/src/dash/DashAdapter.js index 9786085efb..e2cf957e23 100644 --- a/src/dash/DashAdapter.js +++ b/src/dash/DashAdapter.js @@ -385,10 +385,10 @@ function DashAdapter() { } /** - * Returns the ProducerReferenceTime as saved in the DashManifestModel if present + * Returns the ProducerReferenceTimes as saved in the DashManifestModel if present * @param {object} streamInfo * @param {object} mediaInfo - * @returns {object} producerReferenceTime + * @returns {object} producerReferenceTimes * @memberOf module:DashAdapter * @instance */ diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index 57be0ef219..3ff0233514 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -275,10 +275,10 @@ function PlaybackController() { /** * Applys service description Latency and PlaybackRate attributes to liveDelay and catchup settings * @param {Object} manifestInfo - * @param {Array} producerReferenceTimes - All ProducerReferencesTimes in MPD + * @param {Array} prftTimeOffsets - Time offests calculated from ProducerReferenceTime elements * @private */ - function _applyServiceDescription(manifestInfo, timeOffsets) { + function _applyServiceDescription(manifestInfo, prftTimeOffsets) { if (!manifestInfo || !manifestInfo.serviceDescriptions) { return; } @@ -299,7 +299,7 @@ function PlaybackController() { settings.update({ streaming: { delay: { - liveDelay: _calculateOffsetLiveDelay(timeOffsets, llsd.latency), + liveDelay: _calculateDelayFromServiceDescription(prftTimeOffsets, llsd.latency), } } }); @@ -329,20 +329,20 @@ function PlaybackController() { /** * Calculates offset to apply to live delay as described in TS 103 285 Clause 10.20.4 - * @param {Array} timeOffsets + * @param {Array} prftTimeOffsets * @param {Object} llsdLatency * @returns {number} * @private */ - function _calculateOffsetLiveDelay(timeOffsets, llsdLatency) { + function _calculateDelayFromServiceDescription(prftTimeOffsets, llsdLatency) { let to = 0; - let offset = timeOffsets.filter(prt => { + let offset = prftTimeOffsets.filter(prt => { return prt.id === llsdLatency.referenceId; }); // If only one ProducerReferenceTime to generate one TO, then use that regardless of matching ids if (offset.length === 0) { - to = (timeOffsets.length > 0) ? timeOffsets[0].to : 0; + to = (prftTimeOffsets.length > 0) ? prftTimeOffsets[0].to : 0; } else { // If multiple id matches, use the first but this should be invalid to = offset[0].to || 0; From 521543e0848e7f55843eeb1cbc21bebb0246c56d Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Tue, 22 Feb 2022 13:23:00 +0000 Subject: [PATCH 07/18] Add collection of PRFTs from Representations --- src/dash/constants/DashConstants.js | 1 + src/dash/models/DashManifestModel.js | 10 ++++ test/unit/dash.constants.DashConstants.js | 1 + test/unit/dash.models.DashManifestModel.js | 69 +++++++++++++++++----- 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/dash/constants/DashConstants.js b/src/dash/constants/DashConstants.js index fe01bab6d5..3b34c7fa53 100644 --- a/src/dash/constants/DashConstants.js +++ b/src/dash/constants/DashConstants.js @@ -99,6 +99,7 @@ class DashConstants { this.LANG = 'lang'; this.VIEWPOINT = 'Viewpoint'; this.ROLE_ASARRAY = 'Role_asArray'; + this.REPRESENTATION_ASARRAY = 'Representation_asArray'; this.PRODUCERREFERENCETIME_ASARRAY = 'ProducerReferenceTime_asArray'; this.ACCESSIBILITY_ASARRAY = 'Accessibility_asArray'; this.AUDIOCHANNELCONFIGURATION_ASARRAY = 'AudioChannelConfiguration_asArray'; diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index 5fef185629..df1a913e7c 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -165,6 +165,16 @@ function DashManifestModel() { function getProducerReferenceTimesForAdaptation(adaptation) { const prtArray = adaptation && adaptation.hasOwnProperty(DashConstants.PRODUCERREFERENCETIME_ASARRAY) ? adaptation[DashConstants.PRODUCERREFERENCETIME_ASARRAY] : []; + + // ProducerReferenceTime elements can also be contained in Representations + const representationsArray = adaptation && adaptation.hasOwnProperty(DashConstants.REPRESENTATION_ASARRAY) ? adaptation[DashConstants.REPRESENTATION_ASARRAY] : []; + + representationsArray.forEach((rep) => { + if (rep.hasOwnProperty(DashConstants.PRODUCERREFERENCETIME_ASARRAY)) { + prtArray.push(...rep[DashConstants.PRODUCERREFERENCETIME_ASARRAY]); + } + }); + const prtsForAdaptation = []; // Unlikely to have multiple ProducerReferenceTimes. diff --git a/test/unit/dash.constants.DashConstants.js b/test/unit/dash.constants.DashConstants.js index 60dd2a6057..0c6be60bbf 100644 --- a/test/unit/dash.constants.DashConstants.js +++ b/test/unit/dash.constants.DashConstants.js @@ -67,6 +67,7 @@ describe('DashConstants', function () { expect(DashConstants.LANG).to.equal('lang'); expect(DashConstants.VIEWPOINT).to.equal('Viewpoint'); expect(DashConstants.ROLE_ASARRAY).to.equal('Role_asArray'); + expect(DashConstants.REPRESENTATION_ASARRAY).to.equal('Representation_asArray'); expect(DashConstants.PRODUCERREFERENCETIME_ASARRAY).to.equal('ProducerReferenceTime_asArray'); expect(DashConstants.ACCESSIBILITY_ASARRAY).to.equal('Accessibility_asArray'); expect(DashConstants.AUDIOCHANNELCONFIGURATION_ASARRAY).to.equal('AudioChannelConfiguration_asArray'); diff --git a/test/unit/dash.models.DashManifestModel.js b/test/unit/dash.models.DashManifestModel.js index c38e50e553..d60892c89b 100644 --- a/test/unit/dash.models.DashManifestModel.js +++ b/test/unit/dash.models.DashManifestModel.js @@ -1162,25 +1162,64 @@ describe('DashManifestModel', function () { expect(obj[1][DashConstants.PRESENTATION_TIME]).to.equal(1); /* jshint ignore:end */ }); - }); - it('returns ProducerReferenceTimes with correct default attribute values', () => { - const node = { - [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ - { - [DashConstants.ID]: 4, - [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:04Z', - [DashConstants.PRESENTATION_TIME]: 0 - } - ] - }; - const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + it('returns ProducerReferenceTimes with correct default attribute values', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 4, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:04Z', + [DashConstants.PRESENTATION_TIME]: 0 + } + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + expect(obj).to.be.instanceOf(Array); // jshint ignore:line + expect(obj).to.have.lengthOf(1); // jshint ignore:line + expect(obj[0].type).to.equal('encoder'); // jshint ignore:line + }); + + it('returns ProducerReferenceTimes within representations', () => { + const node = { + [DashConstants.REPRESENTATION_ASARRAY] : [ + { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 1, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:01Z', + [DashConstants.PRESENTATION_TIME]: 0 + } + ] + }, + { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 2, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:02Z', + [DashConstants.PRESENTATION_TIME]: 1 + } + ] + }, + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + /* jshint ignore:start */ + expect(obj).to.be.instanceOf(Array); + expect(obj).to.have.lengthOf(2); + expect(obj[0][DashConstants.ID]).to.equal(1); + expect(obj[0][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:01Z'); + expect(obj[0][DashConstants.PRESENTATION_TIME]).to.equal(0); + expect(obj[1][DashConstants.ID]).to.equal(2); + expect(obj[1][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:02Z'); + expect(obj[1][DashConstants.PRESENTATION_TIME]).to.equal(1); + /* jshint ignore:end */ + + }); - expect(obj).to.be.instanceOf(Array); // jshint ignore:line - expect(obj).to.have.lengthOf(1); // jshint ignore:line - expect(obj[0].type).to.equal('encoder'); // jshint ignore:line }); + describe('getSelectionPriority', () => { it('should return 1 when adaptation is not defined', () => { From 0f362fe6f3f0fd4c7e2f9aed237a4a16ec2c29e6 Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Wed, 2 Mar 2022 12:38:36 +0000 Subject: [PATCH 08/18] Check for PRFT over all periods and factor in presentationTimeOffset already being divided by timescale --- src/streaming/controllers/StreamController.js | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index bf639875f8..a38ddabea6 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -1160,44 +1160,44 @@ function StreamController() { */ function _getLiveDelayTimeOffsets(streamInfos) { try { - let timeOffsets = []; - - if (streamInfos && streamInfos.length === 1) { - const streamInfo = streamInfos[0]; + if (streamInfos) { const mediaTypes = [Constants.VIDEO, Constants.AUDIO, Constants.TEXT]; const astInSeconds = adapter.getAvailabilityStartTime() / 1000; - timeOffsets = mediaTypes - .reduce((acc, mediaType) => { - acc = acc.concat(adapter.getAllMediaInfoForType(streamInfo, mediaType)); - return acc; - }, []) - .reduce((acc, mediaInfo) => { - const prts = adapter.getProducerReferenceTimes(streamInfo, mediaInfo); - prts.forEach((prt) => { - const voRepresentations = adapter.getVoRepresentations(mediaInfo); - if (voRepresentations && voRepresentations.length > 0 && voRepresentations[0].adaptation && voRepresentations[0].segmentInfoType === DashConstants.SEGMENT_TEMPLATE) { - const voRep = voRepresentations[0]; - const d = new Date(prt[DashConstants.WALL_CLOCK_TIME]); - const wallClockTime = d.getTime() / 1000; - // TS 103 285 Clause 10.20.4 - // 1) Calculate PRT0 - // i) take the PRT@presentationTime and subtract any ST@presentationTime - // ii) convert this time to seconds by dividing by ST@timescale - // iii) Add this to start time of period that contains PRT. - const prt0 = wallClockTime - (((prt[DashConstants.PRESENTATION_TIME] - voRep[DashConstants.PRESENTATION_TIME_OFFSET]) / voRep[DashConstants.TIMESCALE]) + streamInfo.start); - // 2) Calculate TO between PRT at the start of MPD timeline and the AST - const to = astInSeconds - prt0; - // 3) Not applicable as liveLatency does not consider ST@availabilityTimeOffset - acc.push({id: prt[DashConstants.ID], to}); - } - }); + streamInfos.forEach((streamInfo) => { + const offsets = mediaTypes + .reduce((acc, mediaType) => { + acc = acc.concat(adapter.getAllMediaInfoForType(streamInfo, mediaType)); + return acc; + }, []) + .reduce((acc, mediaInfo) => { + const prts = adapter.getProducerReferenceTimes(streamInfo, mediaInfo); + prts.forEach((prt) => { + const voRepresentations = adapter.getVoRepresentations(mediaInfo); + if (voRepresentations && voRepresentations.length > 0 && voRepresentations[0].adaptation && voRepresentations[0].segmentInfoType === DashConstants.SEGMENT_TEMPLATE) { + const voRep = voRepresentations[0]; + const d = new Date(prt[DashConstants.WALL_CLOCK_TIME]); + const wallClockTime = d.getTime() / 1000; + // TS 103 285 Clause 10.20.4 + // 1) Calculate PRT0 + // i) take the PRT@presentationTime and subtract any ST@presentationTime + // ii) convert this time to seconds by dividing by ST@timescale + // iii) Add this to start time of period that contains PRT. + // N.B presentationTimeOffset is already divided by timescale at this point + const prt0 = wallClockTime - (((prt[DashConstants.PRESENTATION_TIME] / voRep[DashConstants.TIMESCALE]) - voRep[DashConstants.PRESENTATION_TIME_OFFSET]) + streamInfo.start); + // 2) Calculate TO between PRT at the start of MPD timeline and the AST + const to = astInSeconds - prt0; + // 3) Not applicable as liveLatency does not consider ST@availabilityTimeOffset + acc.push({id: prt[DashConstants.ID], to}); + } + }); + return acc; + }, []) - return acc; - }, []) + timeOffsets = timeOffsets.concat(offsets); + }) } - return timeOffsets; } catch (e) { return []; From 77cb57a970bacd34893a73a757f13b4550607ee1 Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Wed, 23 Mar 2022 18:50:19 +0000 Subject: [PATCH 09/18] Corrections and additional test --- src/dash/models/DashManifestModel.js | 2 +- src/dash/vo/ProducerReferenceTime.js | 1 + src/streaming/controllers/StreamController.js | 2 +- test/unit/dash.models.DashManifestModel.js | 34 +++++++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index df1a913e7c..735e14f57c 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -202,7 +202,7 @@ function DashManifestModel() { return; } - // Not intereseted in other attributes for now + // Not interested in other attributes for now // UTC element contained must be same as that in the MPD prtsForAdaptation.push(entry); }) diff --git a/src/dash/vo/ProducerReferenceTime.js b/src/dash/vo/ProducerReferenceTime.js index 8a66353e55..b3e8845476 100644 --- a/src/dash/vo/ProducerReferenceTime.js +++ b/src/dash/vo/ProducerReferenceTime.js @@ -40,6 +40,7 @@ this.applicationScheme = null; this.wallClockTime = null; this.presentationTime = NaN; + this.UTCTiming = null; } } diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index a38ddabea6..6a2ed89640 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -1181,7 +1181,7 @@ function StreamController() { const wallClockTime = d.getTime() / 1000; // TS 103 285 Clause 10.20.4 // 1) Calculate PRT0 - // i) take the PRT@presentationTime and subtract any ST@presentationTime + // i) take the PRT@presentationTime and subtract any ST@presentationTimeOffset // ii) convert this time to seconds by dividing by ST@timescale // iii) Add this to start time of period that contains PRT. // N.B presentationTimeOffset is already divided by timescale at this point diff --git a/test/unit/dash.models.DashManifestModel.js b/test/unit/dash.models.DashManifestModel.js index d60892c89b..3c2a959091 100644 --- a/test/unit/dash.models.DashManifestModel.js +++ b/test/unit/dash.models.DashManifestModel.js @@ -1217,6 +1217,40 @@ describe('DashManifestModel', function () { }); + it('returns ProducerReferenceTimes at both AdaptationSet and Representation level', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 1, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:01Z', + [DashConstants.PRESENTATION_TIME]: 1 + } + ], + [DashConstants.REPRESENTATION_ASARRAY] : [ + { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 2, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:02Z', + [DashConstants.PRESENTATION_TIME]: 2 + } + ] + } + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + /* jshint ignore:start */ + expect(obj).to.be.instanceOf(Array); + expect(obj).to.have.lengthOf(2); + expect(obj[0][DashConstants.ID]).to.equal(1); + expect(obj[0][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:01Z'); + expect(obj[0][DashConstants.PRESENTATION_TIME]).to.equal(1); + expect(obj[1][DashConstants.ID]).to.equal(2); + expect(obj[1][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:02Z'); + expect(obj[1][DashConstants.PRESENTATION_TIME]).to.equal(2); + /* jshint ignore:end */ + }); + }); From 5773ed922217147fe9d72b5d5b95a3faabe30f19 Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Mon, 11 Apr 2022 13:33:51 +0100 Subject: [PATCH 10/18] Apply offset to all latency values --- .../controllers/PlaybackController.js | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index 3ff0233514..171f7bd91c 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -273,7 +273,7 @@ function PlaybackController() { } /** - * Applys service description Latency and PlaybackRate attributes to liveDelay and catchup settings + * Applies service description Latency and PlaybackRate attributes to liveDelay and catchup settings * @param {Object} manifestInfo * @param {Array} prftTimeOffsets - Time offests calculated from ProducerReferenceTime elements * @private @@ -296,19 +296,32 @@ function PlaybackController() { if (llsd) { if (llsd.latency && llsd.latency.target > 0) { logger.debug('Apply LL properties coming from service description. Target Latency (ms):', llsd.latency.target); + let to = _calculateOffsetFromServiceDescription(prftTimeOffsets, llsd.latency); settings.update({ streaming: { delay: { - liveDelay: _calculateDelayFromServiceDescription(prftTimeOffsets, llsd.latency), + // TS 103 285 Clause 10.20.4. 3) Subtract calculated offset from Latency@target converted from milliseconds + // liveLatency does not consider ST@availabilityTimeOffset so leave out that step + liveDelay: ((llsd.latency.target / 1000) - to) } } }); - if (llsd.latency.max && llsd.latency.max > llsd.latency.target ) { + if (llsd.latency.max && llsd.latency.max > llsd.latency.target ) { logger.debug('Apply LL properties coming from service description. Max Latency:', llsd.latency.max); settings.update({ streaming: { liveCatchup: { - maxDrift: (llsd.latency.max - llsd.latency.target) / 1000 + maxDrift: (((llsd.latency.max - llsd.latency.target) / 1000) - to) + } + } + }); + } + if (llsd.latency.min && llsd.latency.min < llsd.latency.target) { + logger.debug('Apply LL properties coming from service description. Min Latency:', llsd.latency.min); + settings.update({ + streaming: { + liveCatchup: { + minDrift: (((llsd.latency.min - llsd.latency.target) / 1000) - to) } } }); @@ -328,14 +341,16 @@ function PlaybackController() { } /** - * Calculates offset to apply to live delay as described in TS 103 285 Clause 10.20.4 + * Calculates offset to apply to latency values as described in TS 103 285 Clause 10.20.4 * @param {Array} prftTimeOffsets * @param {Object} llsdLatency * @returns {number} * @private */ - function _calculateDelayFromServiceDescription(prftTimeOffsets, llsdLatency) { + function _calculateOffsetFromServiceDescription(prftTimeOffsets, llsdLatency) { let to = 0; + if (!('referenceId' in llsdLatency) || prftTimeOffsets.length < 1) return to; + let offset = prftTimeOffsets.filter(prt => { return prt.id === llsdLatency.referenceId; }); @@ -348,9 +363,7 @@ function PlaybackController() { to = offset[0].to || 0; } - // TS 103 285 Clause 10.20.4. 3) Subtract calculated offset from Latency@target converted from milliseconds - // liveLatency does not consider ST@availabilityTimeOffset so leave out that step - return ((llsdLatency.target / 1000) - to) + return to; } function getAvailabilityStartTime() { From 67dce6871d400c2770e1a7a44905c311a67935e9 Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Fri, 18 Feb 2022 15:57:06 +0000 Subject: [PATCH 11/18] Calculate and apply time offsets from ProducerReferenceTime when setting liveDelay from ServiceDescription --- src/dash/DashAdapter.js | 23 ++++++ src/dash/constants/DashConstants.js | 4 + src/dash/models/DashManifestModel.js | 42 ++++++++++- src/dash/vo/ProducerReferenceTime.js | 46 ++++++++++++ src/streaming/controllers/StreamController.js | 56 +++++++++++++- test/unit/dash.DashAdapter.js | 17 ++++- test/unit/dash.constants.DashConstants.js | 4 + test/unit/dash.models.DashManifestModel.js | 74 +++++++++++++++++++ ...reaming.controllers.PlaybackControllers.js | 42 +++++++++-- 9 files changed, 298 insertions(+), 10 deletions(-) create mode 100644 src/dash/vo/ProducerReferenceTime.js diff --git a/src/dash/DashAdapter.js b/src/dash/DashAdapter.js index 67d23f7e92..9786085efb 100644 --- a/src/dash/DashAdapter.js +++ b/src/dash/DashAdapter.js @@ -384,6 +384,28 @@ function DashAdapter() { return realAdaptation; } + /** + * Returns the ProducerReferenceTime as saved in the DashManifestModel if present + * @param {object} streamInfo + * @param {object} mediaInfo + * @returns {object} producerReferenceTime + * @memberOf module:DashAdapter + * @instance + */ + function getProducerReferenceTimes(streamInfo, mediaInfo) { + let id, realAdaptation; + + const selectedVoPeriod = getPeriodForStreamInfo(streamInfo, voPeriods); + id = mediaInfo ? mediaInfo.id : null; + + if (voPeriods.length > 0 && selectedVoPeriod) { + realAdaptation = id ? dashManifestModel.getAdaptationForId(id, voPeriods[0].mpd.manifest, selectedVoPeriod.index) : dashManifestModel.getAdaptationForIndex(mediaInfo ? mediaInfo.index : null, voPeriods[0].mpd.manifest, selectedVoPeriod.index); + } + + if (!realAdaptation) return []; + return dashManifestModel.getProducerReferenceTimesForAdaptation(realAdaptation); + } + /** * Return all EssentialProperties of a Representation * @param {object} representation @@ -1165,6 +1187,7 @@ function DashAdapter() { getAllMediaInfoForType, getAdaptationForType, getRealAdaptation, + getProducerReferenceTimes, getRealPeriodByIndex, getEssentialPropertiesForRepresentation, getVoRepresentations, diff --git a/src/dash/constants/DashConstants.js b/src/dash/constants/DashConstants.js index 182d90fe29..50844da0dc 100644 --- a/src/dash/constants/DashConstants.js +++ b/src/dash/constants/DashConstants.js @@ -90,6 +90,7 @@ class DashConstants { this.ESSENTIAL_PROPERTY = 'EssentialProperty'; this.SUPPLEMENTAL_PROPERTY = 'SupplementalProperty'; this.INBAND_EVENT_STREAM = 'InbandEventStream'; + this.PRODUCER_REFERENCE_TIME = 'ProducerReferenceTime'; this.ACCESSIBILITY = 'Accessibility'; this.ROLE = 'Role'; this.RATING = 'Rating'; @@ -98,6 +99,7 @@ class DashConstants { this.LANG = 'lang'; this.VIEWPOINT = 'Viewpoint'; this.ROLE_ASARRAY = 'Role_asArray'; + this.PRODUCERREFERENCETIME_ASARRAY = 'ProducerReferenceTime_asArray'; this.ACCESSIBILITY_ASARRAY = 'Accessibility_asArray'; this.AUDIOCHANNELCONFIGURATION_ASARRAY = 'AudioChannelConfiguration_asArray'; this.CONTENTPROTECTION_ASARRAY = 'ContentProtection_asArray'; @@ -137,6 +139,8 @@ class DashConstants { this.PUBLISH_TIME = 'publishTime'; this.ORIGINAL_PUBLISH_TIME = 'originalPublishTime'; this.ORIGINAL_MPD_ID = 'mpdId'; + this.WALL_CLOCK_TIME = 'wallClockTime'; + this.PRESENTATION_TIME = 'presentationTime'; } constructor () { diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index 8fdfcbee59..fa69f17f17 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -38,6 +38,7 @@ import UTCTiming from '../vo/UTCTiming'; import Event from '../vo/Event'; import BaseURL from '../vo/BaseURL'; import EventStream from '../vo/EventStream'; +import ProducerReferenceTime from '../vo/ProducerReferenceTime'; import ObjectUtils from '../../streaming/utils/ObjectUtils'; import URLUtils from '../../streaming/utils/URLUtils'; import FactoryMaker from '../../core/FactoryMaker'; @@ -162,6 +163,43 @@ function DashManifestModel() { return getIsTypeOf(adaptation, Constants.IMAGE); } + function getProducerReferenceTimesForAdaptation(adaptation) { + const prtArray = adaptation && adaptation.hasOwnProperty(DashConstants.PRODUCERREFERENCETIME_ASARRAY) ? adaptation[DashConstants.PRODUCERREFERENCETIME_ASARRAY] : []; + const prtsForAdaptation = []; + + // Unlikely to have multiple ProducerReferenceTimes. + prtArray.forEach((prt) => { + const entry = new ProducerReferenceTime(); + + if (prt.hasOwnProperty(DashConstants.ID)) { + entry[DashConstants.ID] = prt[DashConstants.ID]; + } else { + // Ignore. Missing mandatory attribute + return; + } + + if (prt.hasOwnProperty(DashConstants.WALL_CLOCK_TIME)) { + entry[DashConstants.WALL_CLOCK_TIME] = prt[DashConstants.WALL_CLOCK_TIME]; + } else { + // Ignore. Missing mandatory attribute + return; + } + + if (prt.hasOwnProperty(DashConstants.PRESENTATION_TIME)) { + entry[DashConstants.PRESENTATION_TIME] = prt[DashConstants.PRESENTATION_TIME]; + } else { + // Ignore. Missing mandatory attribute + return; + } + + // Not intereseted in other attributes for now + // UTC element contained must be same as that in the MPD + prtsForAdaptation.push(entry); + }) + + return prtsForAdaptation; + } + function getLanguageForAdaptation(adaptation) { let lang = ''; @@ -1122,7 +1160,8 @@ function DashManifestModel() { latency = { target: parseInt(sd[prop].target), max: parseInt(sd[prop].max), - min: parseInt(sd[prop].min) + min: parseInt(sd[prop].min), + referenceId: parseInt(sd[prop].referenceId) }; } else if (prop === DashConstants.SERVICE_DESCRIPTION_PLAYBACK_RATE) { playbackRate = { @@ -1192,6 +1231,7 @@ function DashManifestModel() { getIsTypeOf, getIsText, getIsFragmented, + getProducerReferenceTimesForAdaptation, getLanguageForAdaptation, getViewpointForAdaptation, getRolesForAdaptation, diff --git a/src/dash/vo/ProducerReferenceTime.js b/src/dash/vo/ProducerReferenceTime.js new file mode 100644 index 0000000000..8a66353e55 --- /dev/null +++ b/src/dash/vo/ProducerReferenceTime.js @@ -0,0 +1,46 @@ +/** + * The copyright in this software is being made available under the BSD License, + * included below. This software may be subject to other third party and contributor + * rights, including patent rights, and no such rights are granted under this license. + * + * Copyright (c) 2013, Dash Industry Forum. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * * Neither the name of Dash Industry Forum nor the names of its + * contributors may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +/** + * @class + * @ignore + */ + class ProducerReferenceTime { + constructor() { + this.id = null; + this.inband = false; + this.type = 'encoder'; + this.applicationScheme = null; + this.wallClockTime = null; + this.presentationTime = NaN; + } +} + +export default ProducerReferenceTime; \ No newline at end of file diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index 892e25805e..23b2c7a2c4 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -49,6 +49,7 @@ import DashJSError from '../vo/DashJSError'; import Errors from '../../core/errors/Errors'; import EventController from './EventController'; import ConformanceViolationConstants from '../constants/ConformanceViolationConstants'; +import DashConstants from '../../dash/constants/DashConstants'; const PLAYBACK_ENDED_TIMER_INTERVAL = 200; const DVR_WAITING_OFFSET = 2; @@ -355,7 +356,8 @@ function StreamController() { // Compute and set the live delay if (adapter.getIsDynamic()) { const fragmentDuration = _getFragmentDurationForLiveDelayCalculation(streamsInfo, manifestInfo); - playbackController.computeAndSetLiveDelay(fragmentDuration, manifestInfo); + const timeOffsets = _getLiveDelayTimeOffsets(streamsInfo); + playbackController.computeAndSetLiveDelay(fragmentDuration, timeOffsets, manifestInfo); } // Figure out the correct start time and the correct start period @@ -1177,6 +1179,58 @@ function StreamController() { } } + /** + * Calculates an array of time offsets each with matching ProducerReferenceTime id + * @param {Array} streamInfos + * @returns {Array} + * @private + */ + function _getLiveDelayTimeOffsets(streamInfos) { + try { + + let timeOffsets = []; + + if (streamInfos && streamInfos.length === 1) { + const streamInfo = streamInfos[0]; + const mediaTypes = [Constants.VIDEO, Constants.AUDIO, Constants.TEXT]; + const astInSeconds = adapter.getAvailabilityStartTime() / 1000; + + timeOffsets = mediaTypes + .reduce((acc, mediaType) => { + acc = acc.concat(adapter.getAllMediaInfoForType(streamInfo, mediaType)); + return acc; + }, []) + .reduce((acc, mediaInfo) => { + const prts = adapter.getProducerReferenceTimes(streamInfo, mediaInfo); + prts.forEach((prt) => { + const voRepresentations = adapter.getVoRepresentations(mediaInfo); + if (voRepresentations && voRepresentations.length > 0 && voRepresentations[0].adaptation && voRepresentations[0].segmentInfoType === DashConstants.SEGMENT_TEMPLATE) { + const voRep = voRepresentations[0]; + const d = new Date(prt[DashConstants.WALL_CLOCK_TIME]); + const wallClockTime = d.getTime() / 1000; + // TS 103 285 Clause 10.20.4 + // 1) Calculate PRT0 + // i) take the PRT@presentationTime and subtract any ST@presentationTime + // ii) convert this time to seconds by dividing by ST@timescale + // iii) Add this to start time of period that contains PRT. + const prt0 = wallClockTime - (((prt[DashConstants.PRESENTATION_TIME] - voRep[DashConstants.PRESENTATION_TIME_OFFSET]) / voRep[DashConstants.TIMESCALE]) + streamInfo.start); + // 2) Calculate TO between PRT at the start of MPD timeline and the AST + const to = astInSeconds - prt0; + // 3) Not applicable as liveLatency does not consider ST@availabilityTimeOffset + acc.push({id: prt[DashConstants.ID], to}); + } + }); + + return acc; + }, []) + } + + return timeOffsets; + } catch (e) { + return []; + } + }; + /** * Callback handler after the manifest has been updated. Trigger an update in the adapter and filter unsupported stuff. * Finally attempt UTC sync diff --git a/test/unit/dash.DashAdapter.js b/test/unit/dash.DashAdapter.js index 783b177c6f..c451be8479 100644 --- a/test/unit/dash.DashAdapter.js +++ b/test/unit/dash.DashAdapter.js @@ -44,7 +44,7 @@ const manifest_with_ll_service_description = { ServiceDescription: {}, ServiceDescription_asArray: [{ Scope: { schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019' }, - Latency: { target: 3000, max: 5000, min: 2000 }, + Latency: { target: 3000, max: 5000, min: 2000, referenceId: 7 }, PlaybackRate: { max: 1.5, min: 0.5 } }], Period_asArray: [{ @@ -235,6 +235,20 @@ describe('DashAdapter', function () { expect(realAdaptation).to.be.undefined; // jshint ignore:line }); + it('should return empty array when getProducerReferenceTimes is called and streamInfo parameter is null or undefined', () => { + const producerReferenceTimes = dashAdapter.getProducerReferenceTimes(null, voHelper.getDummyMediaInfo()); + + expect(producerReferenceTimes).to.be.instanceOf(Array); // jshint ignore:line + expect(producerReferenceTimes).to.be.empty; // jshint ignore:line + }); + + it('should return empty array when getProducerReferenceTimes is called and mediaInfo parameter is null or undefined', () => { + const producerReferenceTimes = dashAdapter.getProducerReferenceTimes(voHelper.getDummyStreamInfo(), null); + + expect(producerReferenceTimes).to.be.instanceOf(Array); // jshint ignore:line + expect(producerReferenceTimes).to.be.empty; // jshint ignore:line + }); + it('should return empty array when getUTCTimingSources is called and no period is defined', function () { const timingSources = dashAdapter.getUTCTimingSources(); @@ -470,6 +484,7 @@ describe('DashAdapter', function () { expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.target).equals(3000); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.max).equals(5000); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.min).equals(2000); // jshint ignore:line + expect(streamInfos[0].manifestInfo.serviceDescriptions[0].latency.referenceId).equals(7); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].playbackRate.max).equals(1.5); // jshint ignore:line expect(streamInfos[0].manifestInfo.serviceDescriptions[0].playbackRate.min).equals(0.5); // jshint ignore:line }); diff --git a/test/unit/dash.constants.DashConstants.js b/test/unit/dash.constants.DashConstants.js index cb221fae32..60dd2a6057 100644 --- a/test/unit/dash.constants.DashConstants.js +++ b/test/unit/dash.constants.DashConstants.js @@ -58,6 +58,7 @@ describe('DashConstants', function () { expect(DashConstants.ESSENTIAL_PROPERTY).to.equal('EssentialProperty'); expect(DashConstants.SUPPLEMENTAL_PROPERTY).to.equal('SupplementalProperty'); expect(DashConstants.INBAND_EVENT_STREAM).to.equal('InbandEventStream'); + expect(DashConstants.PRODUCER_REFERENCE_TIME).to.equal('ProducerReferenceTime'); expect(DashConstants.ACCESSIBILITY).to.equal('Accessibility'); expect(DashConstants.ROLE).to.equal('Role'); expect(DashConstants.RATING).to.equal('Rating'); @@ -66,6 +67,7 @@ describe('DashConstants', function () { expect(DashConstants.LANG).to.equal('lang'); expect(DashConstants.VIEWPOINT).to.equal('Viewpoint'); expect(DashConstants.ROLE_ASARRAY).to.equal('Role_asArray'); + expect(DashConstants.PRODUCERREFERENCETIME_ASARRAY).to.equal('ProducerReferenceTime_asArray'); expect(DashConstants.ACCESSIBILITY_ASARRAY).to.equal('Accessibility_asArray'); expect(DashConstants.AUDIOCHANNELCONFIGURATION_ASARRAY).to.equal('AudioChannelConfiguration_asArray'); expect(DashConstants.CONTENTPROTECTION_ASARRAY).to.equal('ContentProtection_asArray'); @@ -92,5 +94,7 @@ describe('DashConstants', function () { expect(DashConstants.DVB_PRIORITY).to.equal('dvb:priority'); expect(DashConstants.DVB_WEIGHT).to.equal('dvb:weight'); expect(DashConstants.SUGGESTED_PRESENTATION_DELAY).to.equal('suggestedPresentationDelay'); + expect(DashConstants.WALL_CLOCK_TIME).to.equal('wallClockTime'); + expect(DashConstants.PRESENTATION_TIME).to.equal('presentationTime'); }); }); diff --git a/test/unit/dash.models.DashManifestModel.js b/test/unit/dash.models.DashManifestModel.js index 029291fcb7..c38e50e553 100644 --- a/test/unit/dash.models.DashManifestModel.js +++ b/test/unit/dash.models.DashManifestModel.js @@ -1107,6 +1107,80 @@ describe('DashManifestModel', function () { }); }); + describe('getProducerReferenceTimesForAdaptation', () => { + it('returns an empty Array when no ProducerReferenceTimes are present on a node', () => { + const node = {}; + + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + expect(obj).to.be.instanceOf(Array); // jshint ignore:line + expect(obj).to.be.empty; // jshint ignore:line + }); + + it('returns an empty Array where a single ProducerReferenceTime element on a node has missing mandatory attributes', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 4, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:00Z' + // missing presentationTime + } + ] + }; + + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + expect(obj).to.be.instanceOf(Array); // jshint ignore:line + expect(obj).to.be.empty; // jshint ignore:line + }); + + it('returns an Array of ProducerReferenceTime elements with mandatory attributes', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 4, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:04Z', + [DashConstants.PRESENTATION_TIME]: 0 + }, + { + [DashConstants.ID]: 5, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:05Z', + [DashConstants.PRESENTATION_TIME]: 1 + } + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + /* jshint ignore:start */ + expect(obj).to.be.instanceOf(Array); + expect(obj).to.have.lengthOf(2); + expect(obj[0][DashConstants.ID]).to.equal(4); + expect(obj[0][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:04Z'); + expect(obj[0][DashConstants.PRESENTATION_TIME]).to.equal(0); + expect(obj[1][DashConstants.ID]).to.equal(5); + expect(obj[1][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:05Z'); + expect(obj[1][DashConstants.PRESENTATION_TIME]).to.equal(1); + /* jshint ignore:end */ + }); + }); + + it('returns ProducerReferenceTimes with correct default attribute values', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 4, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:04Z', + [DashConstants.PRESENTATION_TIME]: 0 + } + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + expect(obj).to.be.instanceOf(Array); // jshint ignore:line + expect(obj).to.have.lengthOf(1); // jshint ignore:line + expect(obj[0].type).to.equal('encoder'); // jshint ignore:line + }); + describe('getSelectionPriority', () => { it('should return 1 when adaptation is not defined', () => { diff --git a/test/unit/streaming.controllers.PlaybackControllers.js b/test/unit/streaming.controllers.PlaybackControllers.js index 63d02c0277..adae021800 100644 --- a/test/unit/streaming.controllers.PlaybackControllers.js +++ b/test/unit/streaming.controllers.PlaybackControllers.js @@ -115,21 +115,21 @@ describe('PlaybackController', function () { }) it('should return NaN if no values specified', function () { - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.be.NaN; }) it('should return live delay if specified in the settings', function () { settings.update({ streaming: { delay: { liveDelay: 20 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(20); }) it('should return live delay based on liveDelayFragmentCount if specified in the settings', function () { settings.update({ streaming: { delay: { liveDelayFragmentCount: 5 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); expect(liveDelay).to.equal(10); }) @@ -152,28 +152,28 @@ describe('PlaybackController', function () { it('should return live delay based on suggestedPresentationDelay', function () { const adapterStub = sinon.stub(adapterMock, 'getSuggestedPresentationDelay').returns(12); - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(12); adapterStub.restore(); }) it('should return live delay based on fragment duration and FRAGMENT_DURATION_FACTOR', function () { - const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); expect(liveDelay).to.equal(8); }) it('should return live delay based on minBufferTime', function () { manifestInfo.minBufferTime = 8; - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); expect(liveDelay).to.equal(32); }) it('should prefer live delay based on liveDelay if both liveDelay and liveDelayFragmentCount are specified in the settings', function () { settings.update({ streaming: { delay: { liveDelayFragmentCount: 5, liveDelay: 40 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); expect(liveDelay).to.equal(40); }) @@ -192,6 +192,34 @@ describe('PlaybackController', function () { expect(liveDelay).to.equal(13); }) + it('should apply calculated offsets to liveDelay if ServiceDescription id matches ProducerReferenceTime referenceId', function () { + manifestInfo.serviceDescriptions = [{ + schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019', + latency: { + referenceId: 7, + target: 13000 + } + }] + const timeOffsets = [{id: 5, to: 3}, {id: 7, to: 4}]; + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, timeOffsets, manifestInfo); + + expect(liveDelay).to.equal(9); + }); + + it('should apply time offset based on only present ProducerReferenceTime even if ids do not match', function () { + manifestInfo.serviceDescriptions = [{ + schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019', + latency: { + referenceId: 7, + target: 13000 + } + }] + const timeOffsets = [{id: 5, to: 3}]; + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, timeOffsets, manifestInfo); + + expect(liveDelay).to.equal(10); + }); + it('should ignore live delay based on ServiceDescription if wrong scheme id is specified', function () { manifestInfo.serviceDescriptions = [{ schemeIdUri: 'urn:dvb:dash:somescheme', From 276c723863192cfa2bc486aff531d445c076298b Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Mon, 21 Feb 2022 15:14:56 +0000 Subject: [PATCH 12/18] Clearer variables and function names --- src/dash/DashAdapter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dash/DashAdapter.js b/src/dash/DashAdapter.js index 9786085efb..e2cf957e23 100644 --- a/src/dash/DashAdapter.js +++ b/src/dash/DashAdapter.js @@ -385,10 +385,10 @@ function DashAdapter() { } /** - * Returns the ProducerReferenceTime as saved in the DashManifestModel if present + * Returns the ProducerReferenceTimes as saved in the DashManifestModel if present * @param {object} streamInfo * @param {object} mediaInfo - * @returns {object} producerReferenceTime + * @returns {object} producerReferenceTimes * @memberOf module:DashAdapter * @instance */ From 3cc07cc65af236d353cd600b1c13184453be7cc5 Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Tue, 22 Feb 2022 13:23:00 +0000 Subject: [PATCH 13/18] Add collection of PRFTs from Representations --- src/dash/constants/DashConstants.js | 1 + src/dash/models/DashManifestModel.js | 10 ++++ test/unit/dash.constants.DashConstants.js | 1 + test/unit/dash.models.DashManifestModel.js | 69 +++++++++++++++++----- 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/dash/constants/DashConstants.js b/src/dash/constants/DashConstants.js index 50844da0dc..4b5b68f521 100644 --- a/src/dash/constants/DashConstants.js +++ b/src/dash/constants/DashConstants.js @@ -99,6 +99,7 @@ class DashConstants { this.LANG = 'lang'; this.VIEWPOINT = 'Viewpoint'; this.ROLE_ASARRAY = 'Role_asArray'; + this.REPRESENTATION_ASARRAY = 'Representation_asArray'; this.PRODUCERREFERENCETIME_ASARRAY = 'ProducerReferenceTime_asArray'; this.ACCESSIBILITY_ASARRAY = 'Accessibility_asArray'; this.AUDIOCHANNELCONFIGURATION_ASARRAY = 'AudioChannelConfiguration_asArray'; diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index fa69f17f17..ff0ddb84d3 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -165,6 +165,16 @@ function DashManifestModel() { function getProducerReferenceTimesForAdaptation(adaptation) { const prtArray = adaptation && adaptation.hasOwnProperty(DashConstants.PRODUCERREFERENCETIME_ASARRAY) ? adaptation[DashConstants.PRODUCERREFERENCETIME_ASARRAY] : []; + + // ProducerReferenceTime elements can also be contained in Representations + const representationsArray = adaptation && adaptation.hasOwnProperty(DashConstants.REPRESENTATION_ASARRAY) ? adaptation[DashConstants.REPRESENTATION_ASARRAY] : []; + + representationsArray.forEach((rep) => { + if (rep.hasOwnProperty(DashConstants.PRODUCERREFERENCETIME_ASARRAY)) { + prtArray.push(...rep[DashConstants.PRODUCERREFERENCETIME_ASARRAY]); + } + }); + const prtsForAdaptation = []; // Unlikely to have multiple ProducerReferenceTimes. diff --git a/test/unit/dash.constants.DashConstants.js b/test/unit/dash.constants.DashConstants.js index 60dd2a6057..0c6be60bbf 100644 --- a/test/unit/dash.constants.DashConstants.js +++ b/test/unit/dash.constants.DashConstants.js @@ -67,6 +67,7 @@ describe('DashConstants', function () { expect(DashConstants.LANG).to.equal('lang'); expect(DashConstants.VIEWPOINT).to.equal('Viewpoint'); expect(DashConstants.ROLE_ASARRAY).to.equal('Role_asArray'); + expect(DashConstants.REPRESENTATION_ASARRAY).to.equal('Representation_asArray'); expect(DashConstants.PRODUCERREFERENCETIME_ASARRAY).to.equal('ProducerReferenceTime_asArray'); expect(DashConstants.ACCESSIBILITY_ASARRAY).to.equal('Accessibility_asArray'); expect(DashConstants.AUDIOCHANNELCONFIGURATION_ASARRAY).to.equal('AudioChannelConfiguration_asArray'); diff --git a/test/unit/dash.models.DashManifestModel.js b/test/unit/dash.models.DashManifestModel.js index c38e50e553..d60892c89b 100644 --- a/test/unit/dash.models.DashManifestModel.js +++ b/test/unit/dash.models.DashManifestModel.js @@ -1162,25 +1162,64 @@ describe('DashManifestModel', function () { expect(obj[1][DashConstants.PRESENTATION_TIME]).to.equal(1); /* jshint ignore:end */ }); - }); - it('returns ProducerReferenceTimes with correct default attribute values', () => { - const node = { - [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ - { - [DashConstants.ID]: 4, - [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:04Z', - [DashConstants.PRESENTATION_TIME]: 0 - } - ] - }; - const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + it('returns ProducerReferenceTimes with correct default attribute values', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 4, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:04Z', + [DashConstants.PRESENTATION_TIME]: 0 + } + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + + expect(obj).to.be.instanceOf(Array); // jshint ignore:line + expect(obj).to.have.lengthOf(1); // jshint ignore:line + expect(obj[0].type).to.equal('encoder'); // jshint ignore:line + }); + + it('returns ProducerReferenceTimes within representations', () => { + const node = { + [DashConstants.REPRESENTATION_ASARRAY] : [ + { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 1, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:01Z', + [DashConstants.PRESENTATION_TIME]: 0 + } + ] + }, + { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 2, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:02Z', + [DashConstants.PRESENTATION_TIME]: 1 + } + ] + }, + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + /* jshint ignore:start */ + expect(obj).to.be.instanceOf(Array); + expect(obj).to.have.lengthOf(2); + expect(obj[0][DashConstants.ID]).to.equal(1); + expect(obj[0][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:01Z'); + expect(obj[0][DashConstants.PRESENTATION_TIME]).to.equal(0); + expect(obj[1][DashConstants.ID]).to.equal(2); + expect(obj[1][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:02Z'); + expect(obj[1][DashConstants.PRESENTATION_TIME]).to.equal(1); + /* jshint ignore:end */ + + }); - expect(obj).to.be.instanceOf(Array); // jshint ignore:line - expect(obj).to.have.lengthOf(1); // jshint ignore:line - expect(obj[0].type).to.equal('encoder'); // jshint ignore:line }); + describe('getSelectionPriority', () => { it('should return 1 when adaptation is not defined', () => { From a3e027d1387d1c9cb3ec5ec18594eecd2bfd4c98 Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Wed, 2 Mar 2022 12:38:36 +0000 Subject: [PATCH 14/18] Check for PRFT over all periods and factor in presentationTimeOffset already being divided by timescale --- src/streaming/controllers/StreamController.js | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index 23b2c7a2c4..a486e6b72c 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -1187,44 +1187,44 @@ function StreamController() { */ function _getLiveDelayTimeOffsets(streamInfos) { try { - let timeOffsets = []; - - if (streamInfos && streamInfos.length === 1) { - const streamInfo = streamInfos[0]; + if (streamInfos) { const mediaTypes = [Constants.VIDEO, Constants.AUDIO, Constants.TEXT]; const astInSeconds = adapter.getAvailabilityStartTime() / 1000; - timeOffsets = mediaTypes - .reduce((acc, mediaType) => { - acc = acc.concat(adapter.getAllMediaInfoForType(streamInfo, mediaType)); - return acc; - }, []) - .reduce((acc, mediaInfo) => { - const prts = adapter.getProducerReferenceTimes(streamInfo, mediaInfo); - prts.forEach((prt) => { - const voRepresentations = adapter.getVoRepresentations(mediaInfo); - if (voRepresentations && voRepresentations.length > 0 && voRepresentations[0].adaptation && voRepresentations[0].segmentInfoType === DashConstants.SEGMENT_TEMPLATE) { - const voRep = voRepresentations[0]; - const d = new Date(prt[DashConstants.WALL_CLOCK_TIME]); - const wallClockTime = d.getTime() / 1000; - // TS 103 285 Clause 10.20.4 - // 1) Calculate PRT0 - // i) take the PRT@presentationTime and subtract any ST@presentationTime - // ii) convert this time to seconds by dividing by ST@timescale - // iii) Add this to start time of period that contains PRT. - const prt0 = wallClockTime - (((prt[DashConstants.PRESENTATION_TIME] - voRep[DashConstants.PRESENTATION_TIME_OFFSET]) / voRep[DashConstants.TIMESCALE]) + streamInfo.start); - // 2) Calculate TO between PRT at the start of MPD timeline and the AST - const to = astInSeconds - prt0; - // 3) Not applicable as liveLatency does not consider ST@availabilityTimeOffset - acc.push({id: prt[DashConstants.ID], to}); - } - }); + streamInfos.forEach((streamInfo) => { + const offsets = mediaTypes + .reduce((acc, mediaType) => { + acc = acc.concat(adapter.getAllMediaInfoForType(streamInfo, mediaType)); + return acc; + }, []) + .reduce((acc, mediaInfo) => { + const prts = adapter.getProducerReferenceTimes(streamInfo, mediaInfo); + prts.forEach((prt) => { + const voRepresentations = adapter.getVoRepresentations(mediaInfo); + if (voRepresentations && voRepresentations.length > 0 && voRepresentations[0].adaptation && voRepresentations[0].segmentInfoType === DashConstants.SEGMENT_TEMPLATE) { + const voRep = voRepresentations[0]; + const d = new Date(prt[DashConstants.WALL_CLOCK_TIME]); + const wallClockTime = d.getTime() / 1000; + // TS 103 285 Clause 10.20.4 + // 1) Calculate PRT0 + // i) take the PRT@presentationTime and subtract any ST@presentationTime + // ii) convert this time to seconds by dividing by ST@timescale + // iii) Add this to start time of period that contains PRT. + // N.B presentationTimeOffset is already divided by timescale at this point + const prt0 = wallClockTime - (((prt[DashConstants.PRESENTATION_TIME] / voRep[DashConstants.TIMESCALE]) - voRep[DashConstants.PRESENTATION_TIME_OFFSET]) + streamInfo.start); + // 2) Calculate TO between PRT at the start of MPD timeline and the AST + const to = astInSeconds - prt0; + // 3) Not applicable as liveLatency does not consider ST@availabilityTimeOffset + acc.push({id: prt[DashConstants.ID], to}); + } + }); + return acc; + }, []) - return acc; - }, []) + timeOffsets = timeOffsets.concat(offsets); + }) } - return timeOffsets; } catch (e) { return []; From a1b26fbccf2d2c63d26f41791354063000233fe1 Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Wed, 23 Mar 2022 18:50:19 +0000 Subject: [PATCH 15/18] Corrections and additional test --- src/dash/models/DashManifestModel.js | 2 +- src/dash/vo/ProducerReferenceTime.js | 1 + src/streaming/controllers/StreamController.js | 2 +- test/unit/dash.models.DashManifestModel.js | 34 +++++++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index ff0ddb84d3..1ced3c38b7 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -202,7 +202,7 @@ function DashManifestModel() { return; } - // Not intereseted in other attributes for now + // Not interested in other attributes for now // UTC element contained must be same as that in the MPD prtsForAdaptation.push(entry); }) diff --git a/src/dash/vo/ProducerReferenceTime.js b/src/dash/vo/ProducerReferenceTime.js index 8a66353e55..b3e8845476 100644 --- a/src/dash/vo/ProducerReferenceTime.js +++ b/src/dash/vo/ProducerReferenceTime.js @@ -40,6 +40,7 @@ this.applicationScheme = null; this.wallClockTime = null; this.presentationTime = NaN; + this.UTCTiming = null; } } diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index a486e6b72c..7c51e4a619 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -1208,7 +1208,7 @@ function StreamController() { const wallClockTime = d.getTime() / 1000; // TS 103 285 Clause 10.20.4 // 1) Calculate PRT0 - // i) take the PRT@presentationTime and subtract any ST@presentationTime + // i) take the PRT@presentationTime and subtract any ST@presentationTimeOffset // ii) convert this time to seconds by dividing by ST@timescale // iii) Add this to start time of period that contains PRT. // N.B presentationTimeOffset is already divided by timescale at this point diff --git a/test/unit/dash.models.DashManifestModel.js b/test/unit/dash.models.DashManifestModel.js index d60892c89b..3c2a959091 100644 --- a/test/unit/dash.models.DashManifestModel.js +++ b/test/unit/dash.models.DashManifestModel.js @@ -1217,6 +1217,40 @@ describe('DashManifestModel', function () { }); + it('returns ProducerReferenceTimes at both AdaptationSet and Representation level', () => { + const node = { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 1, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:01Z', + [DashConstants.PRESENTATION_TIME]: 1 + } + ], + [DashConstants.REPRESENTATION_ASARRAY] : [ + { + [DashConstants.PRODUCERREFERENCETIME_ASARRAY] : [ + { + [DashConstants.ID]: 2, + [DashConstants.WALL_CLOCK_TIME]: '1970-01-01T00:00:02Z', + [DashConstants.PRESENTATION_TIME]: 2 + } + ] + } + ] + }; + const obj = dashManifestModel.getProducerReferenceTimesForAdaptation(node); + /* jshint ignore:start */ + expect(obj).to.be.instanceOf(Array); + expect(obj).to.have.lengthOf(2); + expect(obj[0][DashConstants.ID]).to.equal(1); + expect(obj[0][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:01Z'); + expect(obj[0][DashConstants.PRESENTATION_TIME]).to.equal(1); + expect(obj[1][DashConstants.ID]).to.equal(2); + expect(obj[1][DashConstants.WALL_CLOCK_TIME]).to.equal('1970-01-01T00:00:02Z'); + expect(obj[1][DashConstants.PRESENTATION_TIME]).to.equal(2); + /* jshint ignore:end */ + }); + }); From 1b2ac4359041ccdc7bd13615d6412c632e759223 Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Thu, 14 Apr 2022 12:25:47 +0100 Subject: [PATCH 16/18] Integrate ProducerReferenceTime offset calculations with ServiceDescriptionController --- src/core/Settings.js | 12 +- src/streaming/MediaPlayer.js | 4 + .../ServiceDescriptionController.js | 140 ++++++++++++++++-- src/streaming/controllers/StreamController.js | 60 +------- test/unit/mocks/AdapterMock.js | 11 ++ ...reaming.controllers.PlaybackControllers.js | 42 +----- ...ontrollers.ServiceDescriptionController.js | 57 ++++++- 7 files changed, 218 insertions(+), 108 deletions(-) diff --git a/src/core/Settings.js b/src/core/Settings.js index 6a3e7203bb..f0629f9ef7 100644 --- a/src/core/Settings.js +++ b/src/core/Settings.js @@ -63,6 +63,8 @@ import Events from './events/Events'; * wallclockTimeUpdateInterval: 100, * manifestUpdateRetryInterval: 100, * cacheInitSegments: true, + * applyServiceDescription: true, + * applyProducerReferenceTime: true, * eventControllerRefreshDelay: 100, * capabilities: { * filterUnsupportedEssentialProperties: true, @@ -78,8 +80,7 @@ import Events from './events/Events'; * delay: { * liveDelayFragmentCount: NaN, * liveDelay: NaN, - * useSuggestedPresentationDelay: true, - * applyServiceDescription: true + * useSuggestedPresentationDelay: true * }, * protection: { * keepProtectionMediaKeys: false @@ -237,8 +238,6 @@ import Events from './events/Events'; * If set, this parameter will take precedence over setLiveDelayFragmentCount and manifest info. * @property {boolean} [useSuggestedPresentationDelay=true] * Set to true if you would like to overwrite the default live delay and honor the SuggestedPresentationDelay attribute in by the manifest. - * @property {boolean} [applyServiceDescription=true] - * Set to true if dash.js should use the parameters defined in ServiceDescription elements */ /** @@ -650,6 +649,10 @@ import Events from './events/Events'; * For live streams, set the interval-frequency in milliseconds at which dash.js will check if the current manifest is still processed before downloading the next manifest once the minimumUpdatePeriod time has. * @property {boolean} [cacheInitSegments=true] * Enables the caching of init segments to avoid requesting the init segments before each representation switch. + * @property {boolean} [applyServiceDescription=true] + * Set to true if dash.js should use the parameters defined in ServiceDescription elements + * @property {boolean} [applyProducerReferenceTime=true] + * Set to true if dash.js should use the parameters defined in ProducerReferenceTime elements in combination with ServiceDescription elements. * @property {number} [eventControllerRefreshDelay=100] * Defines the delay in milliseconds between two consecutive checks for events to be fired. * @property {module:Settings~Metrics} metrics Metric settings @@ -752,6 +755,7 @@ function Settings() { manifestUpdateRetryInterval: 100, cacheInitSegments: false, applyServiceDescription: true, + applyProducerReferenceTime: true, eventControllerRefreshDelay: 150, capabilities: { filterUnsupportedEssentialProperties: true, diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js index 58321732d8..a6e2ab0964 100644 --- a/src/streaming/MediaPlayer.js +++ b/src/streaming/MediaPlayer.js @@ -349,6 +349,10 @@ function MediaPlayer() { adapter: adapter }); + serviceDescriptionController.setConfig({ + adapter: adapter + }); + if (!segmentBaseController) { segmentBaseController = SegmentBaseController(context).getInstance({ dashMetrics: dashMetrics, diff --git a/src/streaming/controllers/ServiceDescriptionController.js b/src/streaming/controllers/ServiceDescriptionController.js index 1e66ce7dab..da093b9644 100644 --- a/src/streaming/controllers/ServiceDescriptionController.js +++ b/src/streaming/controllers/ServiceDescriptionController.js @@ -31,6 +31,7 @@ import FactoryMaker from '../../core/FactoryMaker'; import Debug from '../../core/Debug'; import Constants from "../constants/Constants"; +import DashConstants from '../../dash/constants/DashConstants'; const SUPPORTED_SCHEMES = [Constants.SERVICE_DESCRIPTION_DVB_LL_SCHEME]; const MEDIA_TYPES = { @@ -45,13 +46,23 @@ function ServiceDescriptionController() { let instance, serviceDescriptionSettings, - logger; + prftOffsets, + logger, + adapter; function setup() { logger = Debug(context).getInstance().getLogger(instance); _resetInitialSettings(); } + function setConfig(config) { + if (!config) return; + + if (config.adapter) { + adapter = config.adapter; + } + } + function reset() { _resetInitialSettings(); } @@ -67,6 +78,7 @@ function ServiceDescriptionController() { maxBitrate: {}, initialBitrate: {} }; + prftOffsets = []; } /** @@ -112,7 +124,7 @@ function ServiceDescriptionController() { /** * Adjust the latency targets for the service. - * @param {object} sd + * @param {object} sd - service description element * @private */ function _applyServiceDescriptionLatency(sd) { @@ -124,41 +136,60 @@ function ServiceDescriptionController() { params = _getStandardServiceDescriptionLatencyParameters(sd); } - serviceDescriptionSettings.liveDelay = params.liveDelay; - serviceDescriptionSettings.liveCatchup.maxDrift = params.maxDrift; + if (prftOffsets.length > 0) { + let {to, id} = _calculateTimeOffset(params); + + // TS 103 285 Clause 10.20.4. 3) Subtract calculated offset from Latency@target converted from milliseconds + // liveLatency does not consider ST@availabilityTimeOffset so leave out that step + // Since maxDrift is a difference rather than absolute it does not need offset applied + serviceDescriptionSettings.liveDelay = params.liveDelay - to; + serviceDescriptionSettings.liveCatchup.maxDrift = params.maxDrift; - logger.debug(`Found latency properties coming from service description: Live Delay: ${params.liveDelay}, Live catchup max drift: ${params.maxDrift}`); + logger.debug(` + Found latency properties coming from service description. Applied time offset of ${to} from ProducerReferenceTime element with id ${id}. + Live Delay: ${params.liveDelay - to}, Live catchup max drift: ${params.maxDrift} + `); + } else { + serviceDescriptionSettings.liveDelay = params.liveDelay; + serviceDescriptionSettings.liveCatchup.maxDrift = params.maxDrift; + + logger.debug(`Found latency properties coming from service description: Live Delay: ${params.liveDelay}, Live catchup max drift: ${params.maxDrift}`); + } } /** * Get default parameters for liveDelay,maxDrift * @param {object} sd - * @return {{ maxDrift: (number|undefined), liveDelay: number}} + * @return {{maxDrift: (number|undefined), liveDelay: number, referenceId: (number|undefined)}} * @private */ function _getStandardServiceDescriptionLatencyParameters(sd) { const liveDelay = sd.latency.target / 1000; let maxDrift = !isNaN(sd.latency.max) && sd.latency.max > sd.latency.target ? (sd.latency.max - sd.latency.target + 500) / 1000 : NaN; + const referenceId = sd.latency.referenceId || NaN; return { liveDelay, - maxDrift + maxDrift, + referenceId } } /** * Get DVB DASH parameters for liveDelay,maxDrift * @param sd - * @return {{maxDrift: (number|undefined), liveDelay: number}} + * @return {{maxDrift: (number|undefined), liveDelay: number, referenceId: (number|undefined)}} * @private */ function _getDvbServiceDescriptionLatencyParameters(sd) { const liveDelay = sd.latency.target / 1000; let maxDrift = !isNaN(sd.latency.max) && sd.latency.max > sd.latency.target ? (sd.latency.max - sd.latency.target + 500) / 1000 : NaN; + const referenceId = sd.latency.referenceId || NaN; return { liveDelay, - maxDrift + maxDrift, + referenceId } } @@ -242,11 +273,100 @@ function ServiceDescriptionController() { } } + /** + * Returns the current calculated time offsets based on ProducerReferenceTime elements + * @returns {array} + */ + function getProducerReferenceTimeOffsets() { + return prftOffsets; + } + + /** + * Calculates an array of time offsets each with matching ProducerReferenceTime id. + * Call before applyServiceDescription if producer reference time elements should be considered. + * @param {array} streamInfos + * @returns {array} + * @private + */ + function calculateProducerReferenceTimeOffsets(streamInfos) { + try { + let timeOffsets = []; + if (streamInfos && streamInfos.length > 0) { + const mediaTypes = [Constants.VIDEO, Constants.AUDIO, Constants.TEXT]; + const astInSeconds = adapter.getAvailabilityStartTime() / 1000; + + streamInfos.forEach((streamInfo) => { + const offsets = mediaTypes + .reduce((acc, mediaType) => { + acc = acc.concat(adapter.getAllMediaInfoForType(streamInfo, mediaType)); + return acc; + }, []) + .reduce((acc, mediaInfo) => { + const prts = adapter.getProducerReferenceTimes(streamInfo, mediaInfo); + prts.forEach((prt) => { + const voRepresentations = adapter.getVoRepresentations(mediaInfo); + if (voRepresentations && voRepresentations.length > 0 && voRepresentations[0].adaptation && voRepresentations[0].segmentInfoType === DashConstants.SEGMENT_TEMPLATE) { + const voRep = voRepresentations[0]; + const d = new Date(prt[DashConstants.WALL_CLOCK_TIME]); + const wallClockTime = d.getTime() / 1000; + // TS 103 285 Clause 10.20.4 + // 1) Calculate PRT0 + // i) take the PRT@presentationTime and subtract any ST@presentationTimeOffset + // ii) convert this time to seconds by dividing by ST@timescale + // iii) Add this to start time of period that contains PRT. + // N.B presentationTimeOffset is already divided by timescale at this point + const prt0 = wallClockTime - (((prt[DashConstants.PRESENTATION_TIME] / voRep[DashConstants.TIMESCALE]) - voRep[DashConstants.PRESENTATION_TIME_OFFSET]) + streamInfo.start); + // 2) Calculate TO between PRT at the start of MPD timeline and the AST + const to = astInSeconds - prt0; + // 3) Not applicable as liveLatency does not consider ST@availabilityTimeOffset + acc.push({id: prt[DashConstants.ID], to}); + } + }); + return acc; + }, []) + + timeOffsets = timeOffsets.concat(offsets); + }) + } + prftOffsets = timeOffsets; + } catch (e) { + logger.error(e); + prftOffsets = []; + } + }; + + /** + * Calculates offset to apply to live delay as described in TS 103 285 Clause 10.20.4 + * @param {object} sdLatency - service description latency element + * @returns {number} + * @private + */ + function _calculateTimeOffset(sdLatency) { + let to = 0, id; + let offset = prftOffsets.filter(prt => { + return prt.id === sdLatency.referenceId; + }); + + // If only one ProducerReferenceTime to generate one TO, then use that regardless of matching ids + if (offset.length === 0) { + to = (prftOffsets.length > 0) ? prftOffsets[0].to : 0; + id = prftOffsets[0].id || NaN; + } else { + // If multiple id matches, use the first but this should be invalid + to = offset[0].to || 0; + id = offset[0].id || NaN; + } + + return {to, id} + } instance = { getServiceDescriptionSettings, + getProducerReferenceTimeOffsets, + calculateProducerReferenceTimeOffsets, applyServiceDescription, - reset + reset, + setConfig }; setup(); diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index 7c51e4a619..c10a8ea8a3 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -49,7 +49,6 @@ import DashJSError from '../vo/DashJSError'; import Errors from '../../core/errors/Errors'; import EventController from './EventController'; import ConformanceViolationConstants from '../constants/ConformanceViolationConstants'; -import DashConstants from '../../dash/constants/DashConstants'; const PLAYBACK_ENDED_TIMER_INTERVAL = 200; const DVR_WAITING_OFFSET = 2; @@ -348,6 +347,10 @@ function StreamController() { } // Apply Service description parameters. + if (settings.get().streaming.applyProducerReferenceTime) { + serviceDescriptionController.calculateProducerReferenceTimeOffsets(streamsInfo); + }; + const manifestInfo = streamsInfo[0].manifestInfo; if (settings.get().streaming.applyServiceDescription) { serviceDescriptionController.applyServiceDescription(manifestInfo); @@ -356,8 +359,7 @@ function StreamController() { // Compute and set the live delay if (adapter.getIsDynamic()) { const fragmentDuration = _getFragmentDurationForLiveDelayCalculation(streamsInfo, manifestInfo); - const timeOffsets = _getLiveDelayTimeOffsets(streamsInfo); - playbackController.computeAndSetLiveDelay(fragmentDuration, timeOffsets, manifestInfo); + playbackController.computeAndSetLiveDelay(fragmentDuration, manifestInfo); } // Figure out the correct start time and the correct start period @@ -1179,58 +1181,6 @@ function StreamController() { } } - /** - * Calculates an array of time offsets each with matching ProducerReferenceTime id - * @param {Array} streamInfos - * @returns {Array} - * @private - */ - function _getLiveDelayTimeOffsets(streamInfos) { - try { - let timeOffsets = []; - if (streamInfos) { - const mediaTypes = [Constants.VIDEO, Constants.AUDIO, Constants.TEXT]; - const astInSeconds = adapter.getAvailabilityStartTime() / 1000; - - streamInfos.forEach((streamInfo) => { - const offsets = mediaTypes - .reduce((acc, mediaType) => { - acc = acc.concat(adapter.getAllMediaInfoForType(streamInfo, mediaType)); - return acc; - }, []) - .reduce((acc, mediaInfo) => { - const prts = adapter.getProducerReferenceTimes(streamInfo, mediaInfo); - prts.forEach((prt) => { - const voRepresentations = adapter.getVoRepresentations(mediaInfo); - if (voRepresentations && voRepresentations.length > 0 && voRepresentations[0].adaptation && voRepresentations[0].segmentInfoType === DashConstants.SEGMENT_TEMPLATE) { - const voRep = voRepresentations[0]; - const d = new Date(prt[DashConstants.WALL_CLOCK_TIME]); - const wallClockTime = d.getTime() / 1000; - // TS 103 285 Clause 10.20.4 - // 1) Calculate PRT0 - // i) take the PRT@presentationTime and subtract any ST@presentationTimeOffset - // ii) convert this time to seconds by dividing by ST@timescale - // iii) Add this to start time of period that contains PRT. - // N.B presentationTimeOffset is already divided by timescale at this point - const prt0 = wallClockTime - (((prt[DashConstants.PRESENTATION_TIME] / voRep[DashConstants.TIMESCALE]) - voRep[DashConstants.PRESENTATION_TIME_OFFSET]) + streamInfo.start); - // 2) Calculate TO between PRT at the start of MPD timeline and the AST - const to = astInSeconds - prt0; - // 3) Not applicable as liveLatency does not consider ST@availabilityTimeOffset - acc.push({id: prt[DashConstants.ID], to}); - } - }); - return acc; - }, []) - - timeOffsets = timeOffsets.concat(offsets); - }) - } - return timeOffsets; - } catch (e) { - return []; - } - }; - /** * Callback handler after the manifest has been updated. Trigger an update in the adapter and filter unsupported stuff. * Finally attempt UTC sync diff --git a/test/unit/mocks/AdapterMock.js b/test/unit/mocks/AdapterMock.js index 83aa77fda2..6b51dd48c4 100644 --- a/test/unit/mocks/AdapterMock.js +++ b/test/unit/mocks/AdapterMock.js @@ -177,6 +177,17 @@ function AdapterMock () { this.regularPeriods = periods; }; + this.getProducerReferenceTimes = function () { + return [{ + UTCTiming: null, + applicationScheme: null, + id: 7, + inband: false, + presentationTime: 10000, + type: "encoder", + wallClockTime: "1970-01-01T00:00:04Z" + }]; + }; } diff --git a/test/unit/streaming.controllers.PlaybackControllers.js b/test/unit/streaming.controllers.PlaybackControllers.js index adae021800..63d02c0277 100644 --- a/test/unit/streaming.controllers.PlaybackControllers.js +++ b/test/unit/streaming.controllers.PlaybackControllers.js @@ -115,21 +115,21 @@ describe('PlaybackController', function () { }) it('should return NaN if no values specified', function () { - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); expect(liveDelay).to.be.NaN; }) it('should return live delay if specified in the settings', function () { settings.update({ streaming: { delay: { liveDelay: 20 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); expect(liveDelay).to.equal(20); }) it('should return live delay based on liveDelayFragmentCount if specified in the settings', function () { settings.update({ streaming: { delay: { liveDelayFragmentCount: 5 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); expect(liveDelay).to.equal(10); }) @@ -152,28 +152,28 @@ describe('PlaybackController', function () { it('should return live delay based on suggestedPresentationDelay', function () { const adapterStub = sinon.stub(adapterMock, 'getSuggestedPresentationDelay').returns(12); - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); expect(liveDelay).to.equal(12); adapterStub.restore(); }) it('should return live delay based on fragment duration and FRAGMENT_DURATION_FACTOR', function () { - const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); expect(liveDelay).to.equal(8); }) it('should return live delay based on minBufferTime', function () { manifestInfo.minBufferTime = 8; - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); expect(liveDelay).to.equal(32); }) it('should prefer live delay based on liveDelay if both liveDelay and liveDelayFragmentCount are specified in the settings', function () { settings.update({ streaming: { delay: { liveDelayFragmentCount: 5, liveDelay: 40 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); expect(liveDelay).to.equal(40); }) @@ -192,34 +192,6 @@ describe('PlaybackController', function () { expect(liveDelay).to.equal(13); }) - it('should apply calculated offsets to liveDelay if ServiceDescription id matches ProducerReferenceTime referenceId', function () { - manifestInfo.serviceDescriptions = [{ - schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019', - latency: { - referenceId: 7, - target: 13000 - } - }] - const timeOffsets = [{id: 5, to: 3}, {id: 7, to: 4}]; - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, timeOffsets, manifestInfo); - - expect(liveDelay).to.equal(9); - }); - - it('should apply time offset based on only present ProducerReferenceTime even if ids do not match', function () { - manifestInfo.serviceDescriptions = [{ - schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019', - latency: { - referenceId: 7, - target: 13000 - } - }] - const timeOffsets = [{id: 5, to: 3}]; - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, timeOffsets, manifestInfo); - - expect(liveDelay).to.equal(10); - }); - it('should ignore live delay based on ServiceDescription if wrong scheme id is specified', function () { manifestInfo.serviceDescriptions = [{ schemeIdUri: 'urn:dvb:dash:somescheme', diff --git a/test/unit/streaming.controllers.ServiceDescriptionController.js b/test/unit/streaming.controllers.ServiceDescriptionController.js index 2145231f1e..99685b5064 100644 --- a/test/unit/streaming.controllers.ServiceDescriptionController.js +++ b/test/unit/streaming.controllers.ServiceDescriptionController.js @@ -1,15 +1,37 @@ +import AdapterMock from './mocks/AdapterMock'; + const ServiceDescriptionController = require('../../src/streaming/controllers/ServiceDescriptionController'); const expect = require('chai').expect; describe('ServiceDescriptionController', () => { - let serviceDescriptionController; - let dummyManifestInfo; + let serviceDescriptionController, + dummyStreamsInfo, + dummyVoRep, + dummyManifestInfo, + adapterMock; before(() => { const context = {}; - + dummyStreamsInfo = [{ + index: 0, + start: 0 + }] + dummyVoRep = { + adaptation: {}, + presentationTimeOffset: 3, + segmentDuration: 3.84, + segmentInfoType: "SegmentTemplate", + timescale: 1000, + }; + + adapterMock = new AdapterMock(); + adapterMock.setRepresentation(dummyVoRep); serviceDescriptionController = ServiceDescriptionController(context).getInstance(); + + serviceDescriptionController.setConfig({ + adapter: adapterMock + }); }) beforeEach(() => { @@ -19,7 +41,8 @@ describe('ServiceDescriptionController', () => { latency: { target: 5000, max: 8000, - min: 3000 + min: 3000, + referenceId: 7 // Matches PRFT in AdapterMock }, playbackRate: { max: 1.4, @@ -36,6 +59,23 @@ describe('ServiceDescriptionController', () => { serviceDescriptionController.reset(); }) + describe('calculateProducerReferenceTimeOffsets()', () => { + it('Should not throw an error if no streamsInfo provided', () => { + serviceDescriptionController.calculateProducerReferenceTimeOffsets([]); + const prftTimeOffsets = serviceDescriptionController.getProducerReferenceTimeOffsets(); + expect(prftTimeOffsets).to.be.an('array').that.is.empty; + }); + + it('Should calculate expected prtf time offsets', () => { + // Inner workings mostly covered in adapter tests + serviceDescriptionController.calculateProducerReferenceTimeOffsets(dummyStreamsInfo); + const prftTimeOffsets = serviceDescriptionController.getProducerReferenceTimeOffsets(); + expect(prftTimeOffsets).to.be.an('array'); + expect(prftTimeOffsets[0].id).to.equal(7); + expect(prftTimeOffsets[0].to).to.equal(3); + }); + }); + describe('applyServiceDescription()', () => { it('Should not throw an error if no manifestInfo provided', () => { @@ -98,6 +138,15 @@ describe('ServiceDescriptionController', () => { expect(currentSettings.liveCatchup.maxDrift).to.be.NaN; }) + it('Should apply ProducerReferenceTime offsets to latency parameters defined in the ServiceDescription', () => { + // N.B latency@min is not used + serviceDescriptionController.calculateProducerReferenceTimeOffsets(dummyStreamsInfo); + serviceDescriptionController.applyServiceDescription(dummyManifestInfo); + const currentSettings = serviceDescriptionController.getServiceDescriptionSettings(); + expect(currentSettings.liveDelay).to.be.equal(2); + expect(currentSettings.liveCatchup.maxDrift).to.be.equal(3.5); + }); + it('Should not update playback rate if max value is below 1', () => { delete dummyManifestInfo.serviceDescriptions[0].latency; delete dummyManifestInfo.serviceDescriptions[0].operatingBandwidth; From 203fee3b555d68c1649fc0c018ef30797a70fd11 Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Thu, 14 Apr 2022 13:01:08 +0100 Subject: [PATCH 17/18] Revert tests changed in merge --- ...reaming.controllers.PlaybackControllers.js | 42 ++++--------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/test/unit/streaming.controllers.PlaybackControllers.js b/test/unit/streaming.controllers.PlaybackControllers.js index adae021800..63d02c0277 100644 --- a/test/unit/streaming.controllers.PlaybackControllers.js +++ b/test/unit/streaming.controllers.PlaybackControllers.js @@ -115,21 +115,21 @@ describe('PlaybackController', function () { }) it('should return NaN if no values specified', function () { - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); expect(liveDelay).to.be.NaN; }) it('should return live delay if specified in the settings', function () { settings.update({ streaming: { delay: { liveDelay: 20 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); expect(liveDelay).to.equal(20); }) it('should return live delay based on liveDelayFragmentCount if specified in the settings', function () { settings.update({ streaming: { delay: { liveDelayFragmentCount: 5 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); expect(liveDelay).to.equal(10); }) @@ -152,28 +152,28 @@ describe('PlaybackController', function () { it('should return live delay based on suggestedPresentationDelay', function () { const adapterStub = sinon.stub(adapterMock, 'getSuggestedPresentationDelay').returns(12); - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); expect(liveDelay).to.equal(12); adapterStub.restore(); }) it('should return live delay based on fragment duration and FRAGMENT_DURATION_FACTOR', function () { - const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); expect(liveDelay).to.equal(8); }) it('should return live delay based on minBufferTime', function () { manifestInfo.minBufferTime = 8; - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, [], manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(NaN, manifestInfo); expect(liveDelay).to.equal(32); }) it('should prefer live delay based on liveDelay if both liveDelay and liveDelayFragmentCount are specified in the settings', function () { settings.update({ streaming: { delay: { liveDelayFragmentCount: 5, liveDelay: 40 } } }); - const liveDelay = playbackController.computeAndSetLiveDelay(2, [], manifestInfo); + const liveDelay = playbackController.computeAndSetLiveDelay(2, manifestInfo); expect(liveDelay).to.equal(40); }) @@ -192,34 +192,6 @@ describe('PlaybackController', function () { expect(liveDelay).to.equal(13); }) - it('should apply calculated offsets to liveDelay if ServiceDescription id matches ProducerReferenceTime referenceId', function () { - manifestInfo.serviceDescriptions = [{ - schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019', - latency: { - referenceId: 7, - target: 13000 - } - }] - const timeOffsets = [{id: 5, to: 3}, {id: 7, to: 4}]; - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, timeOffsets, manifestInfo); - - expect(liveDelay).to.equal(9); - }); - - it('should apply time offset based on only present ProducerReferenceTime even if ids do not match', function () { - manifestInfo.serviceDescriptions = [{ - schemeIdUri: 'urn:dvb:dash:lowlatency:scope:2019', - latency: { - referenceId: 7, - target: 13000 - } - }] - const timeOffsets = [{id: 5, to: 3}]; - const liveDelay = playbackController.computeAndSetLiveDelay(NaN, timeOffsets, manifestInfo); - - expect(liveDelay).to.equal(10); - }); - it('should ignore live delay based on ServiceDescription if wrong scheme id is specified', function () { manifestInfo.serviceDescriptions = [{ schemeIdUri: 'urn:dvb:dash:somescheme', From f024522cecacbe40b7af5dc74d275c28648f93b9 Mon Sep 17 00:00:00 2001 From: mattjuggins Date: Thu, 14 Apr 2022 15:51:08 +0100 Subject: [PATCH 18/18] Fix StreamController formatting --- src/streaming/controllers/StreamController.js | 3083 ++++++++--------- 1 file changed, 1541 insertions(+), 1542 deletions(-) diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index 7d378a1456..c10a8ea8a3 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -28,1545 +28,1544 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ - import Constants from '../constants/Constants'; - import MetricsConstants from '../constants/MetricsConstants'; - import Stream from '../Stream'; - import ManifestUpdater from '../ManifestUpdater'; - import EventBus from '../../core/EventBus'; - import Events from '../../core/events/Events'; - import FactoryMaker from '../../core/FactoryMaker'; - import { - PlayList, - PlayListTrace - } from '../vo/metrics/PlayList'; - import Debug from '../../core/Debug'; - import InitCache from '../utils/InitCache'; - import URLUtils from '../utils/URLUtils'; - import MediaPlayerEvents from '../MediaPlayerEvents'; - import TimeSyncController from './TimeSyncController'; - import MediaSourceController from './MediaSourceController'; - import DashJSError from '../vo/DashJSError'; - import Errors from '../../core/errors/Errors'; - import EventController from './EventController'; - import ConformanceViolationConstants from '../constants/ConformanceViolationConstants'; - - const PLAYBACK_ENDED_TIMER_INTERVAL = 200; - const DVR_WAITING_OFFSET = 2; - - function StreamController() { - - const context = this.context; - const eventBus = EventBus(context).getInstance(); - - let instance, - logger, - capabilities, - capabilitiesFilter, - manifestUpdater, - manifestLoader, - manifestModel, - adapter, - dashMetrics, - mediaSourceController, - timeSyncController, - baseURLController, - segmentBaseController, - uriFragmentModel, - abrController, - mediaController, - eventController, - initCache, - urlUtils, - errHandler, - timelineConverter, - streams, - activeStream, - protectionController, - textController, - protectionData, - autoPlay, - isStreamSwitchingInProgress, - hasMediaError, - hasInitialisationError, - mediaSource, - videoModel, - playbackController, - serviceDescriptionController, - mediaPlayerModel, - customParametersModel, - isPaused, - initialPlayback, - playbackEndedTimerInterval, - bufferSinks, - preloadingStreams, - supportsChangeType, - settings, - firstLicenseIsFetched, - waitForPlaybackStartTimeout, - errorInformation; - - function setup() { - logger = Debug(context).getInstance().getLogger(instance); - timeSyncController = TimeSyncController(context).getInstance(); - mediaSourceController = MediaSourceController(context).getInstance(); - initCache = InitCache(context).getInstance(); - urlUtils = URLUtils(context).getInstance(); - - resetInitialSettings(); - } - - function initialize(autoPl, protData) { - _checkConfig(); - - autoPlay = autoPl; - protectionData = protData; - timelineConverter.initialize(); - - manifestUpdater = ManifestUpdater(context).create(); - manifestUpdater.setConfig({ - manifestModel: manifestModel, - adapter: adapter, - manifestLoader: manifestLoader, - errHandler: errHandler, - settings: settings - }); - manifestUpdater.initialize(); - - eventController = EventController(context).getInstance(); - eventController.setConfig({ - manifestUpdater: manifestUpdater, - playbackController: playbackController, - settings - }); - eventController.start(); - - - timeSyncController.setConfig({ - dashMetrics, - baseURLController, - errHandler, - settings - }); - timeSyncController.initialize(); - - if (protectionController) { - eventBus.trigger(Events.PROTECTION_CREATED, { - controller: protectionController - }); - protectionController.setMediaElement(videoModel.getElement()); - if (protectionData) { - protectionController.setProtectionData(protectionData); - } - } - - registerEvents(); - } - - function registerEvents() { - eventBus.on(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated, instance); - eventBus.on(MediaPlayerEvents.PLAYBACK_SEEKING, _onPlaybackSeeking, instance); - eventBus.on(MediaPlayerEvents.PLAYBACK_ERROR, _onPlaybackError, instance); - eventBus.on(MediaPlayerEvents.PLAYBACK_STARTED, _onPlaybackStarted, instance); - eventBus.on(MediaPlayerEvents.PLAYBACK_PAUSED, _onPlaybackPaused, instance); - eventBus.on(MediaPlayerEvents.PLAYBACK_ENDED, _onPlaybackEnded, instance); - eventBus.on(MediaPlayerEvents.METRIC_ADDED, _onMetricAdded, instance); - eventBus.on(MediaPlayerEvents.MANIFEST_VALIDITY_CHANGED, _onManifestValidityChanged, instance); - eventBus.on(MediaPlayerEvents.BUFFER_LEVEL_UPDATED, _onBufferLevelUpdated, instance); - eventBus.on(MediaPlayerEvents.QUALITY_CHANGE_REQUESTED, _onQualityChanged, instance); - - if (Events.KEY_SESSION_UPDATED) { - eventBus.on(Events.KEY_SESSION_UPDATED, _onKeySessionUpdated, instance); - } - - eventBus.on(Events.MANIFEST_UPDATED, _onManifestUpdated, instance); - eventBus.on(Events.STREAM_BUFFERING_COMPLETED, _onStreamBufferingCompleted, instance); - eventBus.on(Events.TIME_SYNCHRONIZATION_COMPLETED, _onTimeSyncCompleted, instance); - eventBus.on(Events.CURRENT_TRACK_CHANGED, _onCurrentTrackChanged, instance); - eventBus.on(Events.SETTING_UPDATED_LIVE_DELAY, _onLiveDelaySettingUpdated, instance); - eventBus.on(Events.SETTING_UPDATED_LIVE_DELAY_FRAGMENT_COUNT, _onLiveDelaySettingUpdated, instance); - } - - function unRegisterEvents() { - eventBus.off(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated, instance); - eventBus.off(MediaPlayerEvents.PLAYBACK_SEEKING, _onPlaybackSeeking, instance); - eventBus.off(MediaPlayerEvents.PLAYBACK_ERROR, _onPlaybackError, instance); - eventBus.off(MediaPlayerEvents.PLAYBACK_STARTED, _onPlaybackStarted, instance); - eventBus.off(MediaPlayerEvents.PLAYBACK_PAUSED, _onPlaybackPaused, instance); - eventBus.off(MediaPlayerEvents.PLAYBACK_ENDED, _onPlaybackEnded, instance); - eventBus.off(MediaPlayerEvents.METRIC_ADDED, _onMetricAdded, instance); - eventBus.off(MediaPlayerEvents.MANIFEST_VALIDITY_CHANGED, _onManifestValidityChanged, instance); - eventBus.off(MediaPlayerEvents.BUFFER_LEVEL_UPDATED, _onBufferLevelUpdated, instance); - eventBus.off(MediaPlayerEvents.QUALITY_CHANGE_REQUESTED, _onQualityChanged, instance); - - if (Events.KEY_SESSION_UPDATED) { - eventBus.off(Events.KEY_SESSION_UPDATED, _onKeySessionUpdated, instance); - } - - eventBus.off(Events.MANIFEST_UPDATED, _onManifestUpdated, instance); - eventBus.off(Events.STREAM_BUFFERING_COMPLETED, _onStreamBufferingCompleted, instance); - eventBus.off(Events.TIME_SYNCHRONIZATION_COMPLETED, _onTimeSyncCompleted, instance); - eventBus.off(Events.CURRENT_TRACK_CHANGED, _onCurrentTrackChanged, instance); - eventBus.off(Events.SETTING_UPDATED_LIVE_DELAY, _onLiveDelaySettingUpdated, instance); - eventBus.off(Events.SETTING_UPDATED_LIVE_DELAY_FRAGMENT_COUNT, _onLiveDelaySettingUpdated, instance); - } - - /** - * When the UTC snychronization is completed we can compose the streams - * @private - */ - function _onTimeSyncCompleted( /*e*/) { - _composeStreams(); - } - - /** - * - * @private - */ - function _onKeySessionUpdated() { - firstLicenseIsFetched = true; - } - - /** - * Setup the stream objects after the stream start and each MPD reload. This function is called after the UTC sync has been done (TIME_SYNCHRONIZATION_COMPLETED) - * @private - */ - function _composeStreams() { - try { - const streamsInfo = adapter.getStreamsInfo(); - - if (!activeStream && streamsInfo.length === 0) { - throw new Error('There are no periods in the MPD'); - } - - if (activeStream && streamsInfo.length > 0) { - dashMetrics.updateManifestUpdateInfo({ - currentTime: playbackController.getTime(), - buffered: videoModel.getBufferRange(), - presentationStartTime: streamsInfo[0].start, - clientTimeOffset: timelineConverter.getClientTimeOffset() - }); - } - - // Filter streams that are outdated and not included in the MPD anymore - if (streams.length > 0) { - _filterOutdatedStreams(streamsInfo); - } - - const promises = []; - for (let i = 0, ln = streamsInfo.length; i < ln; i++) { - const streamInfo = streamsInfo[i]; - promises.push(_initializeOrUpdateStream(streamInfo)); - dashMetrics.addManifestUpdateStreamInfo(streamInfo); - } - - Promise.all(promises) - .then(() => { - if (!activeStream) { - _initializeForFirstStream(streamsInfo); - } - - eventBus.trigger(Events.STREAMS_COMPOSED); - // Additional periods might have been added after an MPD update. Check again if we can start prebuffering. - _checkIfPrebufferingCanStart(); - }) - .catch((e) => { - throw e; - }) - - } catch (e) { - errHandler.error(new DashJSError(Errors.MANIFEST_ERROR_ID_NOSTREAMS_CODE, e.message + ' nostreamscomposed', manifestModel.getValue())); - hasInitialisationError = true; - reset(); - } - } - - /** - * Called for each stream when composition is performed. Either a new instance of Stream is created or the existing one is updated. - * @param {object} streamInfo - * @private - */ - function _initializeOrUpdateStream(streamInfo) { - let stream = getStreamById(streamInfo.id); - - // If the Stream object does not exist we probably loaded the manifest the first time or it was - // introduced in the updated manifest, so we need to create a new Stream and perform all the initialization operations - if (!stream) { - stream = Stream(context).create({ - manifestModel, - mediaPlayerModel, - dashMetrics, - manifestUpdater, - adapter, - timelineConverter, - capabilities, - capabilitiesFilter, - errHandler, - baseURLController, - segmentBaseController, - textController, - abrController, - playbackController, - eventController, - mediaController, - protectionController, - videoModel, - streamInfo, - settings - }); - streams.push(stream); - stream.initialize(); - return Promise.resolve(); - } else { - return stream.updateData(streamInfo); - } - } - - /** - * Initialize playback for the first period. - * @param {array} streamsInfo - * @private - */ - function _initializeForFirstStream(streamsInfo) { - - // Add the DVR window so we can calculate the right starting point - addDVRMetric(); - - // If the start is in the future we need to wait - const dvrRange = dashMetrics.getCurrentDVRInfo().range; - if (dvrRange.end < dvrRange.start) { - if (waitForPlaybackStartTimeout) { - clearTimeout(waitForPlaybackStartTimeout); - } - const waitingTime = Math.min((((dvrRange.end - dvrRange.start) * -1) + DVR_WAITING_OFFSET) * 1000, 2147483647); - logger.debug(`Waiting for ${waitingTime} ms before playback can start`); - eventBus.trigger(Events.AST_IN_FUTURE, { delay: waitingTime }); - waitForPlaybackStartTimeout = setTimeout(() => { - _initializeForFirstStream(streamsInfo); - }, waitingTime); - return; - } - - // Apply Service description parameters. - if (settings.get().streaming.applyProducerReferenceTime) { - serviceDescriptionController.calculateProducerReferenceTimeOffsets(streamsInfo); - }; - - const manifestInfo = streamsInfo[0].manifestInfo; - if (settings.get().streaming.applyServiceDescription) { - serviceDescriptionController.applyServiceDescription(manifestInfo); - } - - // Compute and set the live delay - if (adapter.getIsDynamic()) { - const fragmentDuration = _getFragmentDurationForLiveDelayCalculation(streamsInfo, manifestInfo); - playbackController.computeAndSetLiveDelay(fragmentDuration, manifestInfo); - } - - // Figure out the correct start time and the correct start period - const startTime = _getInitialStartTime(); - let initialStream = getStreamForTime(startTime); - const startStream = initialStream !== null ? initialStream : streams[0]; - - eventBus.trigger(Events.INITIAL_STREAM_SWITCH, { startTime }); - _switchStream(startStream, null, startTime); - _startPlaybackEndedTimerInterval(); - } - - /** - * Switch from the current stream (period) to the next stream (period). - * @param {object} stream - * @param {object} previousStream - * @param {number} seekTime - * @private - */ - function _switchStream(stream, previousStream, seekTime) { - try { - if (isStreamSwitchingInProgress || !stream || (previousStream === stream && stream.getIsActive())) { - return; - } - - isStreamSwitchingInProgress = true; - eventBus.trigger(Events.PERIOD_SWITCH_STARTED, { - fromStreamInfo: previousStream ? previousStream.getStreamInfo() : null, - toStreamInfo: stream.getStreamInfo() - }); - - let keepBuffers = false; - activeStream = stream; - - if (previousStream) { - keepBuffers = _canSourceBuffersBeReused(stream, previousStream); - previousStream.deactivate(keepBuffers); - } - - // Determine seek time when switching to new period - // - seek at given seek time - // - or seek at period start if upcoming period is not prebuffered - seekTime = !isNaN(seekTime) ? seekTime : (!keepBuffers && previousStream ? stream.getStreamInfo().start : NaN); - logger.info(`Switch to stream ${stream.getId()}. Seektime is ${seekTime}, current playback time is ${playbackController.getTime()}. Seamless period switch is set to ${keepBuffers}`); - - preloadingStreams = preloadingStreams.filter((s) => { - return s.getId() !== activeStream.getId(); - }); - playbackController.initialize(getActiveStreamInfo(), !!previousStream); - - if (videoModel.getElement()) { - _openMediaSource(seekTime, keepBuffers); - } - } catch (e) { - isStreamSwitchingInProgress = false; - } - } - - /** - * Setup the Media Source. Open MSE and attach event listeners - * @param {number} seekTime - * @param {boolean} keepBuffers - * @private - */ - function _openMediaSource(seekTime, keepBuffers) { - let sourceUrl; - - function _onMediaSourceOpen() { - // Manage situations in which a call to reset happens while MediaSource is being opened - if (!mediaSource || mediaSource.readyState !== 'open') return; - - logger.debug('MediaSource is open!'); - window.URL.revokeObjectURL(sourceUrl); - mediaSource.removeEventListener('sourceopen', _onMediaSourceOpen); - mediaSource.removeEventListener('webkitsourceopen', _onMediaSourceOpen); - - _setMediaDuration(); - const dvrInfo = dashMetrics.getCurrentDVRInfo(); - mediaSourceController.setSeekable(dvrInfo.range.start, dvrInfo.range.end); - _activateStream(seekTime, keepBuffers); - } - - function _open() { - mediaSource.addEventListener('sourceopen', _onMediaSourceOpen, false); - mediaSource.addEventListener('webkitsourceopen', _onMediaSourceOpen, false); - sourceUrl = mediaSourceController.attachMediaSource(videoModel); - logger.debug('MediaSource attached to element. Waiting on open...'); - } - - if (!mediaSource) { - mediaSource = mediaSourceController.createMediaSource(); - _open(); - } else { - if (keepBuffers) { - _activateStream(seekTime, keepBuffers); - } else { - mediaSourceController.detachMediaSource(videoModel); - _open(); - } - } - } - - /** - * Activates a new stream. - * @param {number} seekTime - * @param {boolean} keepBuffers - */ - function _activateStream(seekTime, keepBuffers) { - activeStream.activate(mediaSource, keepBuffers ? bufferSinks : undefined, seekTime) - .then((sinks) => { - // check if change type is supported by the browser - if (sinks) { - const keys = Object.keys(sinks); - if (keys.length > 0 && sinks[keys[0]].getBuffer().changeType) { - supportsChangeType = true; - } - bufferSinks = sinks; - } - - // Set the initial time for this stream in the StreamProcessor - if (!isNaN(seekTime)) { - eventBus.trigger(Events.SEEK_TARGET, { time: seekTime }, { streamId: activeStream.getId() }); - playbackController.seek(seekTime, false, true); - activeStream.startScheduleControllers(); - } - - isStreamSwitchingInProgress = false; - eventBus.trigger(Events.PERIOD_SWITCH_COMPLETED, { toStreamInfo: getActiveStreamInfo() }); - }); - } - - /** - * A playback seeking event was triggered. We need to disable the preloading streams and call the respective seeking handler. - * We distinguish between inner period seeks and outer period seeks - * @param {object} e - * @private - */ - function _onPlaybackSeeking(e) { - const oldTime = playbackController.getTime(); - const newTime = e.seekTime; - const seekToStream = getStreamForTime(newTime); - - if (!seekToStream || seekToStream === activeStream) { - _cancelPreloading(oldTime, newTime); - _handleInnerPeriodSeek(e); - } else if (seekToStream && seekToStream !== activeStream) { - _cancelPreloading(oldTime, newTime, seekToStream); - _handleOuterPeriodSeek(e, seekToStream); - } - - _createPlaylistMetrics(PlayList.SEEK_START_REASON); - } - - /** - * Cancels the preloading of certain streams based on the position we are seeking to. - * @param {number} oldTime - * @param {number} newTime - * @param {boolean} isInnerPeriodSeek - * @private - */ - function _cancelPreloading(oldTime, newTime, seekToStream = null) { - // Inner period seek forward - if (oldTime <= newTime && !seekToStream) { - _deactivateAllPreloadingStreams(); - } - - // Inner period seek: If we seek backwards we might need to prune the period(s) that are currently being prebuffered. For now deactivate everything - else if (oldTime > newTime && !seekToStream) { - _deactivateAllPreloadingStreams(); - } - - // Outer period seek: Deactivate everything for now - else { - _deactivateAllPreloadingStreams(); - } - - } - - /** - * Deactivates all preloading streams - * @private - */ - function _deactivateAllPreloadingStreams() { - if (preloadingStreams && preloadingStreams.length > 0) { - preloadingStreams.forEach((s) => { - s.deactivate(true); - }); - preloadingStreams = []; - } - } - - /** - * Handle an inner period seek. Prepare all StreamProcessors for the seek. - * @param {object} e - * @private - */ - function _handleInnerPeriodSeek(e) { - const streamProcessors = activeStream.getProcessors(); - - streamProcessors.forEach((sp) => { - return sp.prepareInnerPeriodPlaybackSeeking(e); - }); - - _flushPlaylistMetrics(PlayListTrace.USER_REQUEST_STOP_REASON); - } - - /** - * Handle an outer period seek. Dispatch the corresponding event to be handled in the BufferControllers and the ScheduleControllers - * @param {object} e - * @param {object} seekToStream - * @private - */ - function _handleOuterPeriodSeek(e, seekToStream) { - // Stop segment requests - const seekTime = e && !isNaN(e.seekTime) ? e.seekTime : NaN; - const streamProcessors = activeStream.getProcessors(); - - const promises = streamProcessors.map((sp) => { - // Cancel everything in case the active stream is still buffering - return sp.prepareOuterPeriodPlaybackSeeking(e); - }); - - Promise.all(promises) - .then(() => { - _switchStream(seekToStream, activeStream, seekTime); - }) - .catch((e) => { - errHandler.error(e); - }); - } - - /** - * A track change occured. We deactivate the preloading streams - * @param {object} e - * @private - */ - function _onCurrentTrackChanged(e) { - // Track was changed in non active stream. No need to do anything, this only happens when a stream starts preloading - if (e.newMediaInfo.streamInfo.id !== activeStream.getId()) { - return; - } - - // If the track was changed in the active stream we need to stop preloading and remove the already prebuffered stuff. Since we do not support preloading specific handling of specific AdaptationSets yet. - _deactivateAllPreloadingStreams(); - - activeStream.prepareTrackChange(e); - } - - /** - * If the source buffer can be reused we can potentially start buffering the next period - * @param {object} nextStream - * @param {object} previousStream - * @return {boolean} - * @private - */ - function _canSourceBuffersBeReused(nextStream, previousStream) { - try { - // Seamless period switch allowed only if: - // - none of the periods uses contentProtection. - // - AND changeType method implemented by browser or periods use the same codec. - return (settings.get().streaming.buffer.reuseExistingSourceBuffers && (previousStream.isProtectionCompatible(nextStream) || firstLicenseIsFetched) && (supportsChangeType || previousStream.isMediaCodecCompatible(nextStream, previousStream))); - } catch (e) { - return false; - } - } - - /** - * Initiate the preloading of the next stream - * @param {object} nextStream - * @param {object} previousStream - * @private - */ - function _onStreamCanLoadNext(nextStream, previousStream = null) { - - if (mediaSource && !nextStream.getPreloaded()) { - let seamlessPeriodSwitch = _canSourceBuffersBeReused(nextStream, previousStream); - - if (seamlessPeriodSwitch) { - nextStream.startPreloading(mediaSource, bufferSinks) - .then(() => { - preloadingStreams.push(nextStream); - }); - } - } - } - - /** - * Returns the corresponding stream object for a specific presentation time. - * @param {number} time - * @return {null|object} - */ - function getStreamForTime(time) { - - if (isNaN(time)) { - return null; - } - - const ln = streams.length; - - for (let i = 0; i < ln; i++) { - const stream = streams[i]; - const streamEnd = parseFloat((stream.getStartTime() + stream.getDuration()).toFixed(5)); - - if (time < streamEnd) { - return stream; - } - } - - return null; - } - - /** - * Add the DVR window to the metric list. We need the DVR window to restrict the seeking and calculate the right start time. - */ - function addDVRMetric() { - try { - const isDynamic = adapter.getIsDynamic(); - const streamsInfo = adapter.getStreamsInfo(); - const manifestInfo = streamsInfo[0].manifestInfo; - const time = playbackController.getTime(); - const range = timelineConverter.calcTimeShiftBufferWindow(streams, isDynamic); - const activeStreamProcessors = getActiveStreamProcessors(); - - if (typeof range.start === 'undefined' || typeof range.end === 'undefined') { - return; - } - - if (!activeStreamProcessors || activeStreamProcessors.length === 0) { - dashMetrics.addDVRInfo(Constants.VIDEO, time, manifestInfo, range); - } else { - activeStreamProcessors.forEach((sp) => { - dashMetrics.addDVRInfo(sp.getType(), time, manifestInfo, range); - }); - } - } catch (e) { - } - } - - /** - * The buffer level for a certain media type has been updated. If this is the initial playback and we want to autoplay the content we check if we can start playback now. - * For livestreams we might have a drift of the target live delay compared to the current live delay because reaching the initial buffer level took time. - * @param {object} e - * @private - */ - function _onBufferLevelUpdated(e) { - - // check if this is the initial playback and we reached the buffer target. If autoplay is true we start playback - if (initialPlayback && autoPlay) { - const initialBufferLevel = mediaPlayerModel.getInitialBufferLevel(); - const excludedStreamProcessors = [Constants.TEXT]; - if (isNaN(initialBufferLevel) || initialBufferLevel <= playbackController.getBufferLevel(excludedStreamProcessors) || (adapter.getIsDynamic() && initialBufferLevel > playbackController.getLiveDelay())) { - initialPlayback = false; - _createPlaylistMetrics(PlayList.INITIAL_PLAYOUT_START_REASON); - playbackController.play(); - } - } - - if (e && e.mediaType) { - dashMetrics.addBufferLevel(e.mediaType, new Date(), e.bufferLevel * 1000); - } - } - - /** - * When the quality is changed in the currently active stream and we do an aggressive replacement we must stop prebuffering. This is similar to a replacing track switch - * Otherwise preloading can go on. - * @param e - * @private - */ - function _onQualityChanged(e) { - if (e.streamInfo.id === activeStream.getId() && e.reason && e.reason.forceReplace) { - _deactivateAllPreloadingStreams(); - } - - const stream = getStreamById(e.streamInfo.id); - - stream.prepareQualityChange(e); - } - - /** - * A setting related to the live delay was updated. Check if one of the latency values changed. If so, recalculate the live delay. - * @private - */ - function _onLiveDelaySettingUpdated() { - if (adapter.getIsDynamic() && playbackController.getLiveDelay() !== 0) { - const streamsInfo = adapter.getStreamsInfo() - if (streamsInfo.length > 0) { - const manifestInfo = streamsInfo[0].manifestInfo; - const fragmentDuration = _getFragmentDurationForLiveDelayCalculation(streamsInfo, manifestInfo); - - playbackController.computeAndSetLiveDelay(fragmentDuration, manifestInfo); - } - } - } - - /** - * When the playback time is updated we add the droppedFrames metric to the dash metric object - * @private - */ - function _onPlaybackTimeUpdated(/*e*/) { - if (hasVideoTrack()) { - const playbackQuality = videoModel.getPlaybackQuality(); - if (playbackQuality) { - dashMetrics.addDroppedFrames(playbackQuality); - } - } - } - - /** - * Once playback starts add playlist metrics depending on whether this was the first playback or playback resumed after pause - * @private - */ - function _onPlaybackStarted( /*e*/) { - logger.debug('[onPlaybackStarted]'); - if (!initialPlayback && isPaused) { - _createPlaylistMetrics(PlayList.RESUME_FROM_PAUSE_START_REASON); - } - if (initialPlayback) { - initialPlayback = false; - } - isPaused = false; - } - - /** - * Once playback is paused flush metrics - * @param {object} e - * @private - */ - function _onPlaybackPaused(e) { - logger.debug('[onPlaybackPaused]'); - if (!e.ended) { - isPaused = true; - _flushPlaylistMetrics(PlayListTrace.USER_REQUEST_STOP_REASON); - } - } - - /** - * Callback once a stream/period is completely buffered. We can either signal the end of the stream or start prebuffering the next period. - * @param {object} e - * @private - */ - function _onStreamBufferingCompleted(e) { - logger.debug(`Stream with id ${e.streamInfo.id} finished buffering`); - const isLast = e.streamInfo.isLast; - if (mediaSource && isLast) { - logger.info('[onStreamBufferingCompleted] calls signalEndOfStream of mediaSourceController.'); - mediaSourceController.signalEndOfStream(mediaSource); - } else { - _checkIfPrebufferingCanStart(); - } - } - - /** - * Check if we can start prebuffering the next period. - * @private - */ - function _checkIfPrebufferingCanStart() { - // In multiperiod situations, we can start buffering the next stream - if (!activeStream || !activeStream.getHasFinishedBuffering()) { - return; - } - const upcomingStreams = _getNextStreams(activeStream); - let i = 0; - - while (i < upcomingStreams.length) { - const stream = upcomingStreams[i]; - const previousStream = i === 0 ? activeStream : upcomingStreams[i - 1]; - - // If the preloading for the current stream is not scheduled, but its predecessor has finished buffering we can start prebuffering this stream - if (!stream.getPreloaded() && previousStream.getHasFinishedBuffering()) { - if (mediaSource) { - _onStreamCanLoadNext(stream, previousStream); - } - } - i += 1; - } - } - - /** - * In some cases we need to fire the playback ended event manually - * @private - */ - function _startPlaybackEndedTimerInterval() { - if (!playbackEndedTimerInterval) { - playbackEndedTimerInterval = setInterval(function () { - if (!isStreamSwitchingInProgress && playbackController.getTimeToStreamEnd() <= 0 && !playbackController.isSeeking()) { - eventBus.trigger(Events.PLAYBACK_ENDED, { 'isLast': getActiveStreamInfo().isLast }); - } - }, PLAYBACK_ENDED_TIMER_INTERVAL); - } - } - - /** - * Stop the check if the playback has ended - * @private - */ - function _stopPlaybackEndedTimerInterval() { - if (playbackEndedTimerInterval) { - clearInterval(playbackEndedTimerInterval); - playbackEndedTimerInterval = null; - } - } - - /** - * Returns a playhead time, in seconds, converted to be relative - * to the start of an identified stream/period or null if no such stream - * @param {number} time - * @param {string} id - * @returns {number|null} - */ - function getTimeRelativeToStreamId(time, id) { - let stream = null; - let baseStart = 0; - let streamStart = 0; - let streamDur = null; - - for (let i = 0; i < streams.length; i++) { - stream = streams[i]; - streamStart = stream.getStartTime(); - streamDur = stream.getDuration(); - - // use start time, if not undefined or NaN or similar - if (Number.isFinite(streamStart)) { - baseStart = streamStart; - } - - if (stream.getId() === id) { - return time - baseStart; - } else { - // use duration if not undefined or NaN or similar - if (Number.isFinite(streamDur)) { - baseStart += streamDur; - } - } - } - - return null; - } - - /** - * Returns the streamProcessors of the active stream. - * @return {array} - */ - function getActiveStreamProcessors() { - return activeStream ? activeStream.getProcessors() : []; - } - - /** - * Once playback has ended we switch to the next stream - * @param {object} e - */ - function _onPlaybackEnded(e) { - if (activeStream && !activeStream.getIsEndedEventSignaled()) { - activeStream.setIsEndedEventSignaled(true); - const nextStream = _getNextStream(); - if (nextStream) { - logger.debug(`StreamController onEnded, found next stream with id ${nextStream.getStreamInfo().id}. Switching from ${activeStream.getStreamInfo().id} to ${nextStream.getStreamInfo().id}`); - _switchStream(nextStream, activeStream, NaN); - } else { - logger.debug('StreamController no next stream found'); - activeStream.setIsEndedEventSignaled(false); - } - _flushPlaylistMetrics(nextStream ? PlayListTrace.END_OF_PERIOD_STOP_REASON : PlayListTrace.END_OF_CONTENT_STOP_REASON); - } - if (e && e.isLast) { - _stopPlaybackEndedTimerInterval(); - } - } - - /** - * Returns the next stream to be played relative to the stream provided. If no stream is provided we use the active stream. - * In order to avoid rounding issues we should not use the duration of the periods. Instead find the stream with starttime closest to startTime of the previous stream. - * @param {object} stream - * @return {null|object} - */ - function _getNextStream(stream = null) { - const refStream = stream ? stream : activeStream ? activeStream : null; - - if (!refStream) { - return null; - } - - const refStreamInfo = refStream.getStreamInfo(); - const start = refStreamInfo.start; - let i = 0; - let targetIndex = -1; - let lastDiff = NaN; - - while (i < streams.length) { - const s = streams[i]; - const sInfo = s.getStreamInfo(); - const diff = sInfo.start - start; - - if (diff > 0 && (isNaN(lastDiff) || diff < lastDiff) && refStreamInfo.id !== sInfo.id) { - lastDiff = diff; - targetIndex = i; - } - - i += 1; - } - - if (targetIndex >= 0) { - return streams[targetIndex]; - } - - return null; - } - - /** - * Returns all upcoming streams relative to the provided stream. If no stream is provided we use the active stream. - * @param {object} stream - * @return {array} - */ - function _getNextStreams(stream = null) { - try { - const refStream = stream ? stream : activeStream ? activeStream : null; - - if (refStream) { - const refStreamInfo = refStream.getStreamInfo(); - - return streams.filter(function (stream) { - const sInfo = stream.getStreamInfo(); - return sInfo.start > refStreamInfo.start && refStreamInfo.id !== sInfo.id; - }); - } - } catch (e) { - return []; - } - } - - /** - * Sets the duration attribute of the MediaSource using the MediaSourceController. - * @param {number} duration - * @private - */ - function _setMediaDuration(duration) { - const manifestDuration = duration ? duration : getActiveStreamInfo().manifestInfo.duration; - mediaSourceController.setDuration(manifestDuration); - } - - /** - * Returns the active stream - * @return {object} - */ - function getActiveStream() { - return activeStream; - } - - /** - * Initial playback indicates if we have called play() for the first time yet. - * @return {*} - */ - function getInitialPlayback() { - return initialPlayback; - } - - /** - * Auto Play indicates if the stream starts automatically as soon as it is initialized. - * @return {boolean} - */ - function getAutoPlay() { - return autoPlay; - } - - /** - * Called once the first stream has been initialized. We only use this function to seek to the right start time. - * @return {number} - * @private - */ - function _getInitialStartTime() { - // Seek new stream in priority order: - // - at start time provided in URI parameters - // - at stream/period start time (for static streams) or live start time (for dynamic streams) - let startTime; - if (adapter.getIsDynamic()) { - // For dynamic stream, start by default at (live edge - live delay) - 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(); - // 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) { - // #t shall be relative to period start - const startTimeFromUri = _getStartTimeFromUriParameters(true); - if (!isNaN(startTimeFromUri)) { - logger.info('Start time from URI parameters: ' + startTimeFromUri); - // If calcFromSegmentTimeline is enabled we saw problems caused by the MSE.seekableRange when starting at dvrWindow.start. Apply a small offset to avoid this problem. - const offset = settings.get().streaming.timeShiftBuffer.calcFromSegmentTimeline ? 0.1 : 0; - startTime = Math.max(Math.min(startTime, startTimeFromUri), dvrWindow.start + offset); - } - } - } else { - // For static stream, start by default at period start - const streams = getStreams(); - const streamInfo = streams[0].getStreamInfo(); - 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(false); - if (!isNaN(startTimeFromUri)) { - logger.info('Start time from URI parameters: ' + startTimeFromUri); - startTime = Math.max(startTime, startTimeFromUri); - } - } - - return startTime; - } - - /** - * 23009-1 Annex C.4 defines MPD anchors to use URI fragment syntax to start a presentation at a given time and a given state - * @param {boolean} isDynamic - * @return {number} - * @private - */ - function _getStartTimeFromUriParameters(isDynamic) { - const fragData = uriFragmentModel.getURIFragmentData(); - if (!fragData || !fragData.t) { - return NaN; - } - const refStream = getStreams()[0]; - const refStreamStartTime = refStream.getStreamInfo().start; - // 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=