diff --git a/index.d.ts b/index.d.ts index 0e31a15bc0..f50326db6f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -364,7 +364,7 @@ declare namespace dashjs { export type TrackSelectionFunction = (tracks: MediaInfo[]) => MediaInfo[]; export interface MediaPlayerClass { - initialize(view?: HTMLElement, source?: string, autoPlay?: boolean): void; + initialize(view?: HTMLElement, source?: string, autoPlay?: boolean, startTime?: number | string): void; on(type: AstInFutureEvent['type'], listener: (e: AstInFutureEvent) => void, scope?: object): void; @@ -451,7 +451,7 @@ declare namespace dashjs { attachView(element: HTMLElement): void; - attachSource(urlOrManifest: string | object): void; + attachSource(urlOrManifest: string | object, startTime?: number | string): void; isReady(): boolean; diff --git a/samples/advanced/load-with-starttime.html b/samples/advanced/load-with-starttime.html new file mode 100644 index 0000000000..0d9caa8936 --- /dev/null +++ b/samples/advanced/load-with-starttime.html @@ -0,0 +1,99 @@ + + + + + Manual load with start time + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+

Manual load with start time

+

A sample showing how to initialize playback at a specific start time. +

    +
  • For VoD content the start time is relative to the start time of the first period.
  • +
  • For live content +
      +
    • If the parameter starts from prefix + posix: it signifies the absolute time range defined in seconds of Coordinated + Universal Time + (ITU-R TF.460-6). This is the number of seconds since 01-01-1970 00:00:00 UTC. + Fractions of + seconds may be optionally specified down to the millisecond level. +
    • +
    • If no posix prefix is used the starttime is relative to MPD@availabilityStartTime
    • +
    +
  • +
+

+

In this example playback starts 60 seconds from the current wall clock time. +

+
+
+
+ +
+
+
+
+
+
+
+ +
+
+ + + + + + diff --git a/samples/samples.json b/samples/samples.json index 8a88c60764..252090df98 100644 --- a/samples/samples.json +++ b/samples/samples.json @@ -670,6 +670,17 @@ "Video", "Audio" ] + }, + { + "title": "Manual load with start time", + "description": "A sample showing how to initialize playback at a specific start time.", + "href": "advanced/load-with-starttime.html", + "image": "lib/img/bbb-4.jpg", + "labels": [ + "VoD", + "Video", + "Audio" + ] } ] }, diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js index 9907fb8677..89b68c4cfb 100644 --- a/src/streaming/MediaPlayer.js +++ b/src/streaming/MediaPlayer.js @@ -252,14 +252,17 @@ function MediaPlayer() { * * @param {HTML5MediaElement=} view - Optional arg to set the video element. {@link module:MediaPlayer#attachView attachView()} * @param {string=} source - Optional arg to set the media source. {@link module:MediaPlayer#attachSource attachSource()} - * @param {boolean=} AutoPlay - Optional arg to set auto play. {@link module:MediaPlayer#setAutoPlay setAutoPlay()} - * @see {@link module:MediaPlayer#attachView attachView()} + * @param {boolean=} autoPlay - Optional arg to set auto play. {@link module:MediaPlayer#setAutoPlay setAutoPlay()} + * @param {number|string} startTime - For VoD content the start time is relative to the start time of the first period. + * For live content + * If the parameter starts from prefix posix: it signifies the absolute time range defined in seconds of Coordinated Universal Time (ITU-R TF.460-6). This is the number of seconds since 01-01-1970 00:00:00 UTC. Fractions of seconds may be optionally specified down to the millisecond level. + * If no posix prefix is used the starttime is relative to MPD@availabilityStartTime * @see {@link module:MediaPlayer#attachSource attachSource()} * @see {@link module:MediaPlayer#setAutoPlay setAutoPlay()} * @memberof module:MediaPlayer * @instance */ - function initialize(view, source, AutoPlay) { + function initialize(view, source, autoPlay, startTime = NaN) { if (!capabilities) { capabilities = Capabilities(context).getInstance(); capabilities.setConfig({ @@ -381,7 +384,7 @@ function MediaPlayer() { }); restoreDefaultUTCTimingSources(); - setAutoPlay(AutoPlay !== undefined ? AutoPlay : true); + setAutoPlay(autoPlay !== undefined ? autoPlay : true); // Detect and initialize offline module to support offline contents playback _detectOffline(); @@ -392,7 +395,7 @@ function MediaPlayer() { } if (source) { - attachSource(source); + attachSource(source, startTime); } logger.info('[dash.js ' + getVersion() + '] ' + 'MediaPlayer has been initialized'); @@ -1790,14 +1793,17 @@ function MediaPlayer() { * * @param {string|Object} urlOrManifest - A URL to a valid MPD manifest file, or a * parsed manifest object. - * + * @param {number|string} startTime - For VoD content the start time is relative to the start time of the first period. + * For live content + * If the parameter starts from prefix posix: it signifies the absolute time range defined in seconds of Coordinated Universal Time (ITU-R TF.460-6). This is the number of seconds since 01-01-1970 00:00:00 UTC. Fractions of seconds may be optionally specified down to the millisecond level. + * If no posix prefix is used the starttime is relative to MPD@availabilityStartTime * * @throws {@link module:MediaPlayer~MEDIA_PLAYER_NOT_INITIALIZED_ERROR MEDIA_PLAYER_NOT_INITIALIZED_ERROR} if called before initialize function * * @memberof module:MediaPlayer * @instance */ - function attachSource(urlOrManifest) { + function attachSource(urlOrManifest, startTime = NaN) { if (!mediaPlayerInitialized) { throw MEDIA_PLAYER_NOT_INITIALIZED_ERROR; } @@ -1813,7 +1819,7 @@ function MediaPlayer() { } if (isReady()) { - _initializePlayback(); + _initializePlayback(startTime); } } @@ -2262,7 +2268,11 @@ function MediaPlayer() { return utcValue; } - function _initializePlayback() { + /** + * + * @private + */ + function _initializePlayback(startTime = NaN) { if (offlineController) { offlineController.resetRecords(); @@ -2274,9 +2284,9 @@ function MediaPlayer() { _createPlaybackControllers(); if (typeof source === 'string') { - streamController.load(source); + streamController.load(source, startTime); } else { - streamController.loadWithManifest(source); + streamController.loadWithManifest(source, startTime); } } diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index c10a8ea8a3..b689d13a8b 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -36,8 +36,7 @@ import EventBus from '../../core/EventBus'; import Events from '../../core/events/Events'; import FactoryMaker from '../../core/FactoryMaker'; import { - PlayList, - PlayListTrace + PlayList, PlayListTrace } from '../vo/metrics/PlayList'; import Debug from '../../core/Debug'; import InitCache from '../utils/InitCache'; @@ -58,52 +57,14 @@ 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; + 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, providedStartTime, errorInformation; function setup() { logger = Debug(context).getInstance().getLogger(instance); @@ -134,18 +95,13 @@ function StreamController() { eventController = EventController(context).getInstance(); eventController.setConfig({ - manifestUpdater: manifestUpdater, - playbackController: playbackController, - settings + manifestUpdater: manifestUpdater, playbackController: playbackController, settings }); eventController.start(); timeSyncController.setConfig({ - dashMetrics, - baseURLController, - errHandler, - settings + dashMetrics, baseURLController, errHandler, settings }); timeSyncController.initialize(); @@ -210,6 +166,40 @@ function StreamController() { eventBus.off(Events.SETTING_UPDATED_LIVE_DELAY_FRAGMENT_COUNT, _onLiveDelaySettingUpdated, instance); } + function _checkConfig() { + if (!manifestLoader || !manifestLoader.hasOwnProperty('load') || !timelineConverter || !timelineConverter.hasOwnProperty('initialize') || !timelineConverter.hasOwnProperty('reset') || !timelineConverter.hasOwnProperty('getClientTimeOffset') || !manifestModel || !errHandler || !dashMetrics || !playbackController) { + throw new Error(Constants.MISSING_CONFIG_ERROR); + } + } + + function _checkInitialize() { + if (!manifestUpdater || !manifestUpdater.hasOwnProperty('setManifest')) { + throw new Error('initialize function has to be called previously'); + } + } + + /** + * Start the streaming session by loading the target manifest + * @param {string} url + * @param {number} startTime + */ + function load(url, startTime = NaN) { + _checkConfig(); + providedStartTime = startTime; + manifestLoader.load(url); + } + + /** + * Start the streaming session by using the provided manifest object + * @param {object} manifest + * @param {number} startTime + */ + function loadWithManifest(manifest, startTime = NaN) { + _checkInitialize(); + providedStartTime = startTime; + manifestUpdater.setManifest(manifest); + } + /** * When the UTC snychronization is completed we can compose the streams * @private @@ -349,7 +339,8 @@ function StreamController() { // Apply Service description parameters. if (settings.get().streaming.applyProducerReferenceTime) { serviceDescriptionController.calculateProducerReferenceTimeOffsets(streamsInfo); - }; + } + const manifestInfo = streamsInfo[0].manifestInfo; if (settings.get().streaming.applyServiceDescription) { @@ -1030,10 +1021,12 @@ function StreamController() { */ function _getInitialStartTime() { // Seek new stream in priority order: + // - at start time provided via the application // - 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()) { + const isDynamic = adapter.getIsDynamic(); + if (isDynamic) { // For dynamic stream, start by default at (live edge - live delay) const dvrInfo = dashMetrics.getCurrentDVRInfo(); const liveEdge = dvrInfo && dvrInfo.range ? dvrInfo.range.end : 0; @@ -1042,25 +1035,49 @@ function StreamController() { // 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); + // If start time was provided by the application as part of the call to initialize() or attachSource() use this value + if (!isNaN(providedStartTime) || providedStartTime.toString().indexOf('posix:') !== -1) { + logger.info(`Start time provided by the app: ${providedStartTime}`); + const providedStartTimeAsPresentationTime = _getStartTimeFromProvidedData(true, providedStartTime) + if (!isNaN(providedStartTimeAsPresentationTime)) { + // Do not move closer to the live edge as defined by live delay + startTime = Math.min(startTime, providedStartTimeAsPresentationTime); + } + } else { + // #t shall be relative to period start + const startTimeFromUri = _getStartTimeFromUriParameters(true); + if (!isNaN(startTimeFromUri)) { + logger.info(`Start time from URI parameters: ${startTimeFromUri}`); + // Do not move closer to the live edge as defined by live delay + startTime = Math.min(startTime, 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(startTime, 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); + + // If start time was provided by the application as part of the call to initialize() or attachSource() use this value + if (!isNaN(providedStartTime)) { + logger.info(`Start time provided by the app: ${providedStartTime}`); + const providedStartTimeAsPresentationTime = _getStartTimeFromProvidedData(false, providedStartTime) + if (!isNaN(providedStartTimeAsPresentationTime)) { + // Do not play earlier than the start of the first period + startTime = Math.max(startTime, providedStartTimeAsPresentationTime); + } + } else { + // 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}`); + // Do not play earlier than the start of the first period + startTime = Math.max(startTime, startTimeFromUri); + } } } @@ -1079,14 +1096,41 @@ function StreamController() { return NaN; } const refStream = getStreams()[0]; - const refStreamStartTime = refStream.getStreamInfo().start; + const referenceTime = refStream.getStreamInfo().start; + fragData.t = fragData.t.split(',')[0]; + + return _getStartTimeFromString(isDynamic, fragData.t, referenceTime); + } + + /** + * Calculate start time using the value that was provided via the application as part of attachSource() or initialize() + * @param {boolean} isDynamic + * @param {number | string} providedStartTime + * @return {number} + * @private + */ + function _getStartTimeFromProvidedData(isDynamic, providedStartTime) { + let referenceTime = 0; + + if (!isDynamic) { + const refStream = getStreams()[0]; + referenceTime = refStream.getStreamInfo().start; + } + + return _getStartTimeFromString(isDynamic, providedStartTime, referenceTime); + } + + + function _getStartTimeFromString(isDynamic, targetValue, referenceTime) { // 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=