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=