diff --git a/README.md b/README.md index f6bd49290a..553b569bcf 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ For help, join our [Slack channel](https://dashif-slack.azurewebsites.net), our ## Reference players The released [pre-built reference players](http://reference.dashif.org/dash.js/) are publicly accessible if you want direct access without writing any Javascript. +Multiple dash.js samples covering a wide set of common use cases can be found in the project's sample folder hosted [here](http://reference.dashif.org/dash.js/latest/samples/index.html). + The [nightly build of the /dev branch reference player](http://reference.dashif.org/dash.js/nightly/samples/dash-if-reference-player/index.html), is pre-release but contains the latest fixes. It is a good place to start if you are debugging playback problems. A nightly build of the latest minified files are also available: [dash.all.min.js](http://reference.dashif.org/dash.js/nightly/dist/dash.all.min.js) and its debug version [dash.all.debug.js](http://reference.dashif.org/dash.js/nightly/dist/dash.all.debug.js). diff --git a/index.d.ts b/index.d.ts index 21500fd54f..208185288e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -865,7 +865,7 @@ declare namespace dashjs { clearkeys?: { [key: string]: string }; /** Priority level of the key system to be selected (0 is the highest prority, -1 for undefined priority) */ - priority?: number = -1; + priority?: number; } export interface KeySystem { diff --git a/package.json b/package.json index 6d046c52a8..b4efa38884 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dashjs", - "version": "3.0.1", + "version": "3.0.2", "description": "A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers.", "main": "build/es5/index.js", "types": "build/typings/index.d.ts", diff --git a/samples/dash-if-reference-player/app/sources.json b/samples/dash-if-reference-player/app/sources.json index 45d6dffa2c..94fc9b2a7b 100644 --- a/samples/dash-if-reference-player/app/sources.json +++ b/samples/dash-if-reference-player/app/sources.json @@ -807,7 +807,7 @@ "url": "https://profficialsite.origin.mediaservices.windows.net/c51358ea-9a5e-4322-8951-897d640fdfd7/tearsofsteel_4k.ism/manifest(format=mpd-time-csf)", "protData": { "com.microsoft.playready": { - "serverURL": "http://test.playready.microsoft.com/service/rightsmanager.asmx?cfg=(persist:true,sl:150)", + "serverURL": "https://test.playready.microsoft.com/service/rightsmanager.asmx?cfg=(persist:true,sl:150)", "sessionType": "persistent-license" } } diff --git a/src/core/Version.js b/src/core/Version.js index 428615b98c..4c1a777929 100644 --- a/src/core/Version.js +++ b/src/core/Version.js @@ -1,4 +1,4 @@ -const VERSION = '3.0.1'; +const VERSION = '3.0.2'; export function getVersionString() { return VERSION; } diff --git a/src/dash/DashHandler.js b/src/dash/DashHandler.js index cd3ab3126c..7ef2850c31 100644 --- a/src/dash/DashHandler.js +++ b/src/dash/DashHandler.js @@ -388,7 +388,7 @@ function DashHandler(config) { } if (segments.length > 0) { - representation.segmentAvailabilityRange = {start: segments[0].presentationStartTime, end: segments[len - 1].presentationStartTime}; + representation.segmentAvailabilityRange = {start: segments[0].presentationStartTime, end: segments[segments.length - 1].presentationStartTime}; representation.availableSegmentsNumber = segments.length; representation.segments = segments; diff --git a/src/dash/parser/matchers/DurationMatcher.js b/src/dash/parser/matchers/DurationMatcher.js index f25fc4fd52..c5c3d972c6 100644 --- a/src/dash/parser/matchers/DurationMatcher.js +++ b/src/dash/parser/matchers/DurationMatcher.js @@ -66,12 +66,12 @@ class DurationMatcher extends BaseMatcher { str => { //str = "P10Y10M10DT10H10M10.1S"; const match = durationRegex.exec(str); - let result = (parseFloat(match[2] || 0) * SECONDS_IN_YEAR + - parseFloat(match[4] || 0) * SECONDS_IN_MONTH + - parseFloat(match[6] || 0) * SECONDS_IN_DAY + - parseFloat(match[8] || 0) * SECONDS_IN_HOUR + - parseFloat(match[10] || 0) * SECONDS_IN_MIN + - parseFloat(match[12] || 0)); + let result = (parseFloat(match[3] || 0) * SECONDS_IN_YEAR + + parseFloat(match[5] || 0) * SECONDS_IN_MONTH + + parseFloat(match[7] || 0) * SECONDS_IN_DAY + + parseFloat(match[9] || 0) * SECONDS_IN_HOUR + + parseFloat(match[11] || 0) * SECONDS_IN_MIN + + parseFloat(match[13] || 0)); if (match[1] !== undefined) { result = -result; diff --git a/src/dash/utils/SegmentsUtils.js b/src/dash/utils/SegmentsUtils.js index 05b070b32a..b521503e1b 100644 --- a/src/dash/utils/SegmentsUtils.js +++ b/src/dash/utils/SegmentsUtils.js @@ -149,7 +149,15 @@ function isSegmentAvailable(timelineConverter, representation, segment, isDynami const segmentTime = timelineConverter.calcPeriodRelativeTimeFromMpdRelativeTime(representation, segment.presentationStartTime); if (segmentTime >= periodRelativeEnd) { - return false; + if (isDynamic) { + // segment is not available in current period, but it may be segment available in another period that current one (in DVR window) + // if not (time > segmentAvailabilityRange.end), then return false + if ( representation.segmentAvailabilityRange && segment.presentationStartTime >= representation.segmentAvailabilityRange.end) { + return false; + } + } else { + return false; + } } return true; diff --git a/src/streaming/ManifestUpdater.js b/src/streaming/ManifestUpdater.js index c7e6a8c43a..f98a2f214e 100644 --- a/src/streaming/ManifestUpdater.js +++ b/src/streaming/ManifestUpdater.js @@ -107,7 +107,7 @@ function ManifestUpdater() { function stopManifestRefreshTimer() { if (refreshTimer !== null) { - clearInterval(refreshTimer); + clearTimeout(refreshTimer); refreshTimer = null; } } diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js index 203009bdbe..10cf89fb0d 100644 --- a/src/streaming/MediaPlayer.js +++ b/src/streaming/MediaPlayer.js @@ -1373,14 +1373,13 @@ function MediaPlayer() { /** * This method allows to set media settings that will be used to pick the initial track. Format of the settings - * is following: - * {lang: langValue, + * is following:
+ * {lang: langValue (can be either a string or a regex to match), * viewpoint: viewpointValue, * audioChannelConfiguration: audioChannelConfigurationValue, * accessibility: accessibilityValue, * role: roleValue} * - * * @param {string} type * @param {Object} value * @memberof module:MediaPlayer diff --git a/src/streaming/controllers/BufferController.js b/src/streaming/controllers/BufferController.js index ef2b430cba..cff7ec6d75 100644 --- a/src/streaming/controllers/BufferController.js +++ b/src/streaming/controllers/BufferController.js @@ -735,6 +735,7 @@ function BufferController(config) { if (!bufferResetInProgress) { logger.debug('onRemoved : call updateBufferLevel'); updateBufferLevel(); + addBufferMetrics(); } else { bufferResetInProgress = false; if (mediaChunk) { diff --git a/src/streaming/controllers/MediaController.js b/src/streaming/controllers/MediaController.js index f3a61e8052..4a7fc12b46 100644 --- a/src/streaming/controllers/MediaController.js +++ b/src/streaming/controllers/MediaController.js @@ -359,7 +359,7 @@ function MediaController() { } function matchSettings(settings, track) { - const matchLang = !settings.lang || (settings.lang === track.lang); + const matchLang = !settings.lang || (track.lang.match(settings.lang)); const matchViewPoint = !settings.viewpoint || (settings.viewpoint === track.viewpoint); const matchRole = !settings.role || !!track.roles.filter(function (item) { return item === settings.role; diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index 007587c63c..2269733860 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -51,7 +51,7 @@ function PlaybackController() { timelineConverter, liveStartTime, wallclockTimeIntervalId, - commonEarliestTime, + earliestTime, liveDelay, bufferedRange, streamInfo, @@ -90,6 +90,7 @@ function PlaybackController() { eventBus.on(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, this); eventBus.on(Events.BYTES_APPENDED_END_FRAGMENT, onBytesAppended, this); + eventBus.on(Events.BUFFER_CLEARED, onBufferCleared, this); eventBus.on(Events.LOADING_PROGRESS, onFragmentLoadProgress, this); eventBus.on(Events.BUFFER_LEVEL_STATE_CHANGED, onBufferLevelStateChanged, this); eventBus.on(Events.PERIOD_SWITCH_STARTED, onPeriodSwitchStarted, this); @@ -105,9 +106,9 @@ function PlaybackController() { } function onPeriodSwitchStarted(e) { - if (!isDynamic && e.fromStreamInfo && commonEarliestTime[e.fromStreamInfo.id] !== undefined) { + if (!isDynamic && e.fromStreamInfo && earliestTime[e.fromStreamInfo.id] !== undefined) { delete bufferedRange[e.fromStreamInfo.id]; - delete commonEarliestTime[e.fromStreamInfo.id]; + delete earliestTime[e.fromStreamInfo.id]; } } @@ -150,15 +151,11 @@ function PlaybackController() { // Internal seek = seek video model only (disable 'seeking' listener), // buffer(s) are already appended at given time (see onBytesAppended()) videoModel.removeEventListener('seeking', onPlaybackSeeking); - logger.info('Requesting seek to time: ' + time); + logger.info('Requesting internal seek to time: ' + time); videoModel.setCurrentTime(time, stickToBuffered); } } else { eventBus.trigger(Events.PLAYBACK_SEEK_ASKED); - if (streamInfo) { - delete bufferedRange[streamInfo.id]; - delete commonEarliestTime[streamInfo.id]; - } logger.info('Requesting seek to time: ' + time); videoModel.setCurrentTime(time, stickToBuffered); } @@ -228,9 +225,16 @@ function PlaybackController() { function computeLiveDelay(fragmentDuration, dvrWindowSize) { let delay, ret, + r, startTime; const END_OF_PLAYLIST_PADDING = 10; + let uriParameters = uriFragmentModel.getURIFragmentData(); + + if (uriParameters) { + r = parseInt(uriParameters.r, 10); + } + let suggestedPresentationDelay = adapter.getSuggestedPresentationDelay(); if (settings.get().streaming.useSuggestedPresentationDelay && suggestedPresentationDelay !== null) { @@ -239,7 +243,10 @@ function PlaybackController() { delay = 0; } else if (mediaPlayerModel.getLiveDelay()) { delay = mediaPlayerModel.getLiveDelay(); // If set by user, this value takes precedence - } else if (!isNaN(fragmentDuration)) { + } else if (r) { + delay = r; + } + else if (!isNaN(fragmentDuration)) { delay = fragmentDuration * settings.get().streaming.liveDelayFragmentCount; } else { delay = streamInfo.manifestInfo.minBufferTime * 2; @@ -284,7 +291,7 @@ function PlaybackController() { function reset() { liveStartTime = NaN; playOnceInitialized = false; - commonEarliestTime = {}; + earliestTime = {}; liveDelay = 0; availabilityStartTime = 0; bufferedRange = {}; @@ -298,6 +305,7 @@ function PlaybackController() { eventBus.off(Events.PLAYBACK_TIME_UPDATED, onPlaybackProgression, this); eventBus.off(Events.PLAYBACK_ENDED, onPlaybackEnded, this); eventBus.off(Events.STREAM_INITIALIZING, onStreamInitializing, this); + eventBus.off(Events.BUFFER_CLEARED, onBufferCleared, this); stopUpdatingWallclockTime(); removeAllListeners(); } @@ -388,8 +396,8 @@ function PlaybackController() { if (!isNaN(startTimeOffset) && startTimeOffset < Math.max(streamInfo.manifestInfo.duration, streamInfo.duration) && startTimeOffset >= 0) { presentationStartTime = startTimeOffset; } else { - let earliestTime = commonEarliestTime[streamInfo.id]; //set by ready bufferStart after first onBytesAppended - presentationStartTime = earliestTime !== undefined ? Math.max(earliestTime.audio !== undefined ? earliestTime.audio : 0, earliestTime.video !== undefined ? earliestTime.video : 0, streamInfo.start) : streamInfo.start; + let currentEarliestTime = earliestTime[streamInfo.id]; //set by ready bufferStart after first onBytesAppended + presentationStartTime = currentEarliestTime !== undefined ? Math.max(currentEarliestTime.audio !== undefined ? currentEarliestTime.audio : 0, currentEarliestTime.video !== undefined ? currentEarliestTime.video : 0, streamInfo.start) : streamInfo.start; } } } @@ -402,10 +410,14 @@ function PlaybackController() { const DVRWindow = DVRMetrics ? DVRMetrics.range : null; let actualTime; - if (!DVRWindow) return NaN; + if (!DVRWindow) { + return NaN; + } if (currentTime > DVRWindow.end) { actualTime = Math.max(DVRWindow.end - streamInfo.manifestInfo.minBufferTime * 2, DVRWindow.start); - } else if (currentTime + 0.250 < DVRWindow.start && DVRWindow.start - currentTime > DVRWindow.start - 315360000) { + + } else if (currentTime > 0 && currentTime + 0.250 < DVRWindow.start && Math.abs(currentTime - DVRWindow.start) < 315360000) { + // Checking currentTime plus 250ms as the 'timeupdate' is fired with a frequency between 4Hz and 66Hz // https://developer.mozilla.org/en-US/docs/Web/Events/timeupdate // http://w3c.github.io/html/single-page.html#offsets-into-the-media-resource @@ -671,12 +683,25 @@ function PlaybackController() { } } + function onBufferCleared(e) { + const type = e.sender.getType(); + + if (streamInfo && earliestTime[streamInfo.id] && (earliestTime[streamInfo.id][type] >= e.from && earliestTime[streamInfo.id][type] <= e.to)) { + logger.info('Reset commonEarliestTime and bufferedRange for ' + type); + bufferedRange[streamInfo.id][type] = undefined; + earliestTime[streamInfo.id][type] = undefined; + earliestTime[streamInfo.id].started = false; + } else { + logger.info('No need to reset commonEarliestTime and bufferedRange for ' + type); + } + } + function onBytesAppended(e) { - let earliestTime, + let commonEarliestTime, initialStartTime; let ranges = e.bufferedRanges; if (!ranges || !ranges.length) return; - if (commonEarliestTime[streamInfo.id] && commonEarliestTime[streamInfo.id].started === true) { + if (earliestTime[streamInfo.id] && earliestTime[streamInfo.id].started === true) { //stream has already been started. return; } @@ -689,51 +714,51 @@ function PlaybackController() { bufferedRange[streamInfo.id][type] = ranges; - if (commonEarliestTime[streamInfo.id] === undefined) { - commonEarliestTime[streamInfo.id] = []; - commonEarliestTime[streamInfo.id].started = false; + if (earliestTime[streamInfo.id] === undefined) { + earliestTime[streamInfo.id] = []; + earliestTime[streamInfo.id].started = false; } - if (commonEarliestTime[streamInfo.id][type] === undefined) { - commonEarliestTime[streamInfo.id][type] = Math.max(ranges.start(0), streamInfo.start); + if (earliestTime[streamInfo.id][type] === undefined) { + earliestTime[streamInfo.id][type] = Math.max(ranges.start(0), streamInfo.start); } const hasVideoTrack = streamController.isTrackTypePresent(Constants.VIDEO); const hasAudioTrack = streamController.isTrackTypePresent(Constants.AUDIO); - initialStartTime = getStreamStartTime(true); + initialStartTime = getStreamStartTime(false); if (hasAudioTrack && hasVideoTrack) { //current stream has audio and video contents - if (!isNaN(commonEarliestTime[streamInfo.id].audio) && !isNaN(commonEarliestTime[streamInfo.id].video)) { + if (!isNaN(earliestTime[streamInfo.id].audio) && !isNaN(earliestTime[streamInfo.id].video)) { - if (commonEarliestTime[streamInfo.id].audio < commonEarliestTime[streamInfo.id].video) { + if (earliestTime[streamInfo.id].audio < earliestTime[streamInfo.id].video) { // common earliest is video time // check buffered audio range has video time, if ok, we seek, otherwise, we wait some other data - earliestTime = commonEarliestTime[streamInfo.id].video > initialStartTime ? commonEarliestTime[streamInfo.id].video : initialStartTime; + commonEarliestTime = earliestTime[streamInfo.id].video > initialStartTime ? earliestTime[streamInfo.id].video : initialStartTime; ranges = bufferedRange[streamInfo.id].audio; } else { // common earliest is audio time // check buffered video range has audio time, if ok, we seek, otherwise, we wait some other data - earliestTime = commonEarliestTime[streamInfo.id].audio > initialStartTime ? commonEarliestTime[streamInfo.id].audio : initialStartTime; + commonEarliestTime = earliestTime[streamInfo.id].audio > initialStartTime ? earliestTime[streamInfo.id].audio : initialStartTime; ranges = bufferedRange[streamInfo.id].video; } - if (checkTimeInRanges(earliestTime, ranges)) { + if (checkTimeInRanges(commonEarliestTime, ranges)) { if (!(checkTimeInRanges(getNormalizedTime(), bufferedRange[streamInfo.id].audio) && checkTimeInRanges(getNormalizedTime(), bufferedRange[streamInfo.id].video))) { - if (!compatibleWithPreviousStream && earliestTime !== 0) { - seek(earliestTime, true, true); + if (!compatibleWithPreviousStream && commonEarliestTime !== 0) { + seek(commonEarliestTime, true, true); } } - commonEarliestTime[streamInfo.id].started = true; + earliestTime[streamInfo.id].started = true; } } } else { //current stream has only audio or only video content - if (commonEarliestTime[streamInfo.id][type]) { - earliestTime = commonEarliestTime[streamInfo.id][type] > initialStartTime ? commonEarliestTime[streamInfo.id][type] : initialStartTime; + if (earliestTime[streamInfo.id][type]) { + commonEarliestTime = earliestTime[streamInfo.id][type] > initialStartTime ? earliestTime[streamInfo.id][type] : initialStartTime; if (!compatibleWithPreviousStream) { - seek(earliestTime, false, true); + seek(commonEarliestTime, false, true); } - commonEarliestTime[streamInfo.id].started = true; + earliestTime[streamInfo.id].started = true; } } } diff --git a/src/streaming/protection/vo/ProtectionData.js b/src/streaming/protection/vo/ProtectionData.js index 0178ebb988..03c819fe4f 100644 --- a/src/streaming/protection/vo/ProtectionData.js +++ b/src/streaming/protection/vo/ProtectionData.js @@ -46,12 +46,14 @@ class ProtectionData { * @param {Object} clearkeys defines a set of clear keys that are available to * the key system. Object properties are base64-encoded keyIDs (with no padding). * Corresponding property values are keys, base64-encoded (no padding). + * @param {number|undefined} priority priority order of the current ProtectionData * @class */ - constructor(serverURL, httpRequestHeaders, clearkeys) { + constructor(serverURL, httpRequestHeaders, clearkeys, priority) { this.serverURL = serverURL; this.httpRequestHeaders = httpRequestHeaders; this.clearkeys = clearkeys; + this.priority = priority === undefined ? -1 : priority; } } @@ -87,4 +89,14 @@ class ProtectionData { * @memberof ProtectionData */ +/** + * priority + * + * @instance + * @type Object + * @name ProtectionData.priority + * @readonly + * @memberof ProtectionData + */ + export default ProtectionData; diff --git a/test/unit/mocks/URIFragmentModelMock.js b/test/unit/mocks/URIFragmentModelMock.js index f97d966011..dffd1e74a5 100644 --- a/test/unit/mocks/URIFragmentModelMock.js +++ b/test/unit/mocks/URIFragmentModelMock.js @@ -1,9 +1,14 @@ class URIFragmentModelMock { constructor() { + this.uriFragmentData = null; } getURIFragmentData() { - return {t: 18.2}; + return this.uriFragmentData; + } + + setURIFragmentData(uri) { + this.uriFragmentData = uri; } } diff --git a/test/unit/streaming.controllers.MediaController.js b/test/unit/streaming.controllers.MediaController.js index 6de35d2b1f..c8e220c795 100644 --- a/test/unit/streaming.controllers.MediaController.js +++ b/test/unit/streaming.controllers.MediaController.js @@ -239,7 +239,7 @@ describe('MediaController', function () { }); it('getTracksFor should return an empty array if parameters are defined, but internal tracks array is empty', function () { - const trackArray = mediaController.getTracksFor(Constants.VIDEO,{id: 'id'}); + const trackArray = mediaController.getTracksFor(Constants.VIDEO, {id: 'id'}); expect(trackArray).to.be.instanceOf(Array); // jshint ignore:line expect(trackArray).to.be.empty; // jshint ignore:line @@ -435,6 +435,85 @@ describe('MediaController', function () { }); + it('should check initial media settings to choose initial track with a string/regex lang', function () { + const streamInfo = { + id: 'id' + }; + const track = { + type: trackType, + streamInfo: streamInfo, + lang: 'fr', + viewpoint: 'viewpoint', + roles: 1, + accessibility: 1, + audioChannelConfiguration: 1 + }; + + mediaController.addTrack(track); + + let trackList = mediaController.getTracksFor(trackType, streamInfo); + expect(trackList).to.have.lengthOf(1); + expect(objectUtils.areEqual(trackList[0], track)).to.be.true; // jshint ignore:line + + let currentTrack = mediaController.getCurrentTrackFor(trackType, streamInfo); + expect(objectUtils.areEqual(currentTrack, track)).to.be.false; // jshint ignore:line + + // call to checkInitialMediaSettingsForType + mediaController.setInitialSettings(trackType, { + lang: 'fr|en|qtz', + viewpoint: 'viewpoint' + }); + mediaController.checkInitialMediaSettingsForType(trackType, streamInfo); + + currentTrack = mediaController.getCurrentTrackFor(trackType, streamInfo); + expect(objectUtils.areEqual(currentTrack, track)).to.be.true; // jshint ignore:line + }); + + it('should check initial media settings to choose initial track with a regex lang', function () { + const streamInfo = { + id: 'id' + }; + const frTrack = { + type: trackType, + streamInfo: streamInfo, + lang: 'fr', + viewpoint: 'viewpoint', + roles: 1, + accessibility: 1, + audioChannelConfiguration: 1 + }; + const qtzTrack = { + type: trackType, + streamInfo: streamInfo, + lang: 'qtz', + viewpoint: 'viewpoint', + roles: 1, + accessibility: 1, + audioChannelConfiguration: 1 + }; + + mediaController.addTrack(frTrack); + mediaController.addTrack(qtzTrack); + + let trackList = mediaController.getTracksFor(trackType, streamInfo); + expect(trackList).to.have.lengthOf(2); + expect(objectUtils.areEqual(trackList[0], frTrack)).to.be.true; // jshint ignore:line + expect(objectUtils.areEqual(trackList[1], qtzTrack)).to.be.true; // jshint ignore:line + + let currentTrack = mediaController.getCurrentTrackFor(trackType, streamInfo); + expect(objectUtils.areEqual(currentTrack, frTrack)).to.be.false; // jshint ignore:line + expect(objectUtils.areEqual(currentTrack, qtzTrack)).to.be.false; // jshint ignore:line + + // call to checkInitialMediaSettingsForType + mediaController.setInitialSettings(trackType, { + lang: /qtz|mis/, + viewpoint: 'viewpoint' + }); + mediaController.checkInitialMediaSettingsForType(trackType, streamInfo); + + currentTrack = mediaController.getCurrentTrackFor(trackType, streamInfo); + expect(objectUtils.areEqual(currentTrack, qtzTrack)).to.be.true; // jshint ignore:line + }); }); }); diff --git a/test/unit/streaming.controllers.PlaybackControllers.js b/test/unit/streaming.controllers.PlaybackControllers.js index b3849b6899..c74ed0cdbf 100644 --- a/test/unit/streaming.controllers.PlaybackControllers.js +++ b/test/unit/streaming.controllers.PlaybackControllers.js @@ -165,6 +165,7 @@ describe('PlaybackController', function () { }); it('getStartTimeFromUriParameters should return the expected value', function () { + uriFragmentModelMock.setURIFragmentData({t: 18.2}); const uriParameters = playbackController.getStartTimeFromUriParameters(); expect(uriParameters.fragT).to.exist; // jshint ignore:line expect(uriParameters.fragT).to.equal(18.2);