diff --git a/src/core/Settings.js b/src/core/Settings.js index aaf42b0c60..2961cd5711 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/dash/DashAdapter.js b/src/dash/DashAdapter.js index 67d23f7e92..e2cf957e23 100644 --- a/src/dash/DashAdapter.js +++ b/src/dash/DashAdapter.js @@ -384,6 +384,28 @@ function DashAdapter() { return realAdaptation; } + /** + * Returns the ProducerReferenceTimes as saved in the DashManifestModel if present + * @param {object} streamInfo + * @param {object} mediaInfo + * @returns {object} producerReferenceTimes + * @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..4b5b68f521 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,8 @@ 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'; this.CONTENTPROTECTION_ASARRAY = 'ContentProtection_asArray'; @@ -137,6 +140,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..1ced3c38b7 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,53 @@ function DashManifestModel() { return getIsTypeOf(adaptation, Constants.IMAGE); } + 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. + 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 interested 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 +1170,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 +1241,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..b3e8845476 --- /dev/null +++ b/src/dash/vo/ProducerReferenceTime.js @@ -0,0 +1,47 @@ +/** + * 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; + this.UTCTiming = null; + } +} + +export default ProducerReferenceTime; \ No newline at end of file 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 892e25805e..c10a8ea8a3 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -347,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); 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..0c6be60bbf 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,8 @@ 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'); expect(DashConstants.CONTENTPROTECTION_ASARRAY).to.equal('ContentProtection_asArray'); @@ -92,5 +95,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..3c2a959091 100644 --- a/test/unit/dash.models.DashManifestModel.js +++ b/test/unit/dash.models.DashManifestModel.js @@ -1107,6 +1107,153 @@ 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 + }); + + 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 */ + + }); + + 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 */ + }); + + }); + + describe('getSelectionPriority', () => { it('should return 1 when adaptation is not defined', () => { 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.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;