Clock synchronization
During playback of dynamic presentations, a wall clock is used as the timing reference for DASH client decisions. This is a synchronized clock shared by the DASH client and service.
It is critical to synchronize the clocks of the DASH client and service when using a dynamic presentation because the MPD timeline of a dynamic presentation is mapped to wall clock time and many playback decisions are clock driven and assume a common understanding of time by the DASH client and service.
Clock synchronization mechanisms are described by UTCTiming elements in the MPD ([DASH] 5.8.4.11). For further information please check the References [1] and [2].
Clock synchronization in dash.js
dash.js supports multiple schemeIdUri and value combinations for clock synchronization:
urn:mpeg:dash:utc:http-head:2014
urn:mpeg:dash:utc:http-xsdate:2014
urn:mpeg:dash:utc:http-iso:2014
urn:mpeg:dash:utc:direct:2014
urn:mpeg:dash:utc:http-head:2012
urn:mpeg:dash:utc:http-xsdate:2012
urn:mpeg:dash:utc:http-iso:2012
urn:mpeg:dash:utc:direct:2012
The default timing source in dash.js uses the following schemeIdUri
/ value
combination and can be configured as follows:
player.updateSettings({
streaming: {
utcSynchronization: {
defaultTimingSource: {
scheme: 'urn:mpeg:dash:utc:http-xsdate:2014',
value: 'https://time.akamai.com/?iso&ms'
}
}
}
});
UTCTiming
elements in the MPD take precedence over the default timing source specified in the settings.
Regular synchronization
By default, dash.js performs a clock synchronization at playback start and after each MPD update.
Synchronization at startup
At playback start an initial request to the timing server is issued. The offset between the client and the server clock is calculated as described in the Section Offset calculation.
In addition, dash.js performs a predefined number of background requests to verify the initially calculated offset. The number of background attempts can be adjusted in the settings:
player.updateSettings({
streaming: {
utcSynchronization: {
backgroundAttempts: 2
}
}
});
Synchronization after MPD updates
By default, dash.js initiates a synchronization request after each MPD update. This behavior is modified by certain settings parameters. The general workflow is as follows:
An MPD update triggers an event to attempt a clock synchronization. The TimeSyncController
handles the event and checks if a synchronization request is to be made:
function _shouldPerformSynchronization() {
try {
const timeBetweenSyncAttempts = !isNaN(internalTimeBetweenSyncAttempts) ? internalTimeBetweenSyncAttempts : DEFAULT_TIME_BETWEEN_SYNC_ATTEMPTS;
if (!timeOfLastSync || !timeBetweenSyncAttempts || isNaN(timeBetweenSyncAttempts)) {
return true;
}
return ((Date.now() - timeOfLastSync) / 1000) >= timeBetweenSyncAttempts;
} catch (e) {
return true;
}
}
_shouldPerformSynchronization()
compares the current wallclock time against the time of the last sync attempt. If the difference is larger than timeBetweenSyncAttempts
a synchronization request is issued. Otherwise, playback continues without a clock sync.
The initial time between the sync attempts can be configured the following way:
player.updateSettings({
streaming: {
utcSynchronization: {
timeBetweenSyncAttempts: 30
}
}
});
Post-synchronization parameter adjustment
After each regular synchronization attempt, dash.js adjusts its internal internalTimeBetweenSyncAttempts
parameter based on certain criteria:
function _adjustTimeBetweenSyncAttempts(offset) {
const isOffsetDriftWithinThreshold = _isOffsetDriftWithinThreshold(offset);
const timeBetweenSyncAttempts = !isNaN(internalTimeBetweenSyncAttempts) ? internalTimeBetweenSyncAttempts : DEFAULT_TIME_BETWEEN_SYNC_ATTEMPTS;
const timeBetweenSyncAttemptsAdjustmentFactor = !isNaN(settings.get().streaming.utcSynchronization.timeBetweenSyncAttemptsAdjustmentFactor) ? settings.get().streaming.utcSynchronization.timeBetweenSyncAttemptsAdjustmentFactor : DEFAULT_TIME_BETWEEN_SYNC_ATTEMPTS_ADJUSTMENT_FACTOR;
const maximumTimeBetweenSyncAttempts = !isNaN(settings.get().streaming.utcSynchronization.maximumTimeBetweenSyncAttempts) ? settings.get().streaming.utcSynchronization.maximumTimeBetweenSyncAttempts : DEFAULT_MAXIMUM_TIME_BETWEEN_SYNC;
const minimumTimeBetweenSyncAttempts = !isNaN(settings.get().streaming.utcSynchronization.minimumTimeBetweenSyncAttempts) ? settings.get().streaming.utcSynchronization.minimumTimeBetweenSyncAttempts : DEFAULT_MINIMUM_TIME_BETWEEN_SYNC;
let adjustedTimeBetweenSyncAttempts;
if (isOffsetDriftWithinThreshold) {
// The drift between the current offset and the last offset is within the allowed threshold. Increase sync time
adjustedTimeBetweenSyncAttempts = Math.min(timeBetweenSyncAttempts * timeBetweenSyncAttemptsAdjustmentFactor, maximumTimeBetweenSyncAttempts);
logger.debug(`Increasing timeBetweenSyncAttempts to ${adjustedTimeBetweenSyncAttempts}`);
} else {
// Drift between the current offset and the last offset is not within the allowed threshold. Decrease sync time
adjustedTimeBetweenSyncAttempts = Math.max(timeBetweenSyncAttempts / timeBetweenSyncAttemptsAdjustmentFactor, minimumTimeBetweenSyncAttempts);
logger.debug(`Decreasing timeBetweenSyncAttempts to ${adjustedTimeBetweenSyncAttempts}`);
}
internalTimeBetweenSyncAttempts = adjustedTimeBetweenSyncAttempts;
}
In the first step the player checks if the offset is within certain boundaries:
function _isOffsetDriftWithinThreshold(offset) {
try {
if (isNaN(lastOffset)) {
return true;
}
const maxAllowedDrift = settings.get().streaming.utcSynchronization.maximumAllowedDrift && !isNaN(settings.get().streaming.utcSynchronization.maximumAllowedDrift) ? settings.get().streaming.utcSynchronization.maximumAllowedDrift : DEFAULT_MAXIMUM_ALLOWED_DRIFT;
const lowerBound = lastOffset - maxAllowedDrift;
const upperBound = lastOffset + maxAllowedDrift;
return offset >= lowerBound && offset <= upperBound;
} catch (e) {
return true;
}
}
Depending on whether the offset is included in the calculated boundaries, adjustedTimeBetweenSyncAttempts
is derived by either multiplying or dividing the current internalTimeBetweenSyncAttempts
by timeBetweenSyncAttemptsAdjustmentFactor
. By assigning specific values to maximumTimeBetweenSyncAttempts
and minimumTimeBetweenSyncAttempts
upper and lower bounds for internalTimeBetweenSyncAttempts
can be set.
The parameters can be adjusted in the settings:
player.updateSettings({
streaming: {
utcSynchronization: {
timeBetweenSyncAttempts: 30,
maximumTimeBetweenSyncAttempts: 600,
minimumTimeBetweenSyncAttempts: 2,
timeBetweenSyncAttemptsAdjustmentFactor: 2,
maximumAllowedDrift: 100,
}
}
})
Synchronization after download errors
In addition to regular synchronization attempts, dash.js triggers a background synchronization in case requests to media segments result in errors (e.g 404 errors). This is to make sure that the client clock is still synchronized and the request error is not caused by an erroneous offset.
This feature can be enabled/disabled by adjusting the settings:
player.updateSettings({
streaming: {
utcSynchronization: {
enableBackgroundSyncAfterSegmentDownloadError: true
}
}
})
Offset calculation
The offset between two consecutive synchronization requests is calculated by accounting for the round trip time:
function _calculateOffset(deviceTimeBeforeSync, deviceTimeAfterSync, serverTime) {
const deviceReferenceTime = deviceTimeAfterSync - ((deviceTimeAfterSync - deviceTimeBeforeSync) / 2);
return serverTime - deviceReferenceTime;
}
Configuration example
The available configuration parameters:
Parameter | Description |
---|---|
enable | Enable/Disable UTC Time synchronization. |
useManifestDateHeaderTimeSource | Allows you to enable the use of the Date Header, if exposed with CORS, as a timing source for live edge detection. The use of the date header will happen only after the other timing source that take precedence fail or are omitted as described. |
backgroundAttempts | Number of synchronization attempts to perform in the background after an initial synchronization request has been done. This is used to verify that the derived client-server offset is correct. |
timeBetweenSyncAttempts | The time in seconds between two consecutive sync attempts. Note: This value is used as an initial starting value. The internal value of the TimeSyncController is adjusted during playback based on the drift between two consecutive synchronization attempts. |
maximumTimeBetweenSyncAttempts | The maximum time in seconds between two consecutive sync attempts. |
minimumTimeBetweenSyncAttempts | The minimum time in seconds between two consecutive sync attempts |
timeBetweenSyncAttemptsAdjustmentFactor | The factor used to multiply or divide the timeBetweenSyncAttempts parameter after a sync. The maximumAllowedDrift defines whether this value is used as a factor or a dividend. |
maximumAllowedDrift | The maximum allowed drift specified in milliseconds between two consecutive synchronization attempts. |
enableBackgroundSyncAfterSegmentDownloadError | Enables or disables the background sync after the player ran into a segment download error. |
defaultTimingSource | The default timing source to be used. The timing sources in the MPD take precedence over this one. |
An example of a full configuration object looks the following:
player.updateSettings({
streaming: {
utcSynchronization: {
enable: true,
useManifestDateHeaderTimeSource: true,
backgroundAttempts: 2,
timeBetweenSyncAttempts: 30,
maximumTimeBetweenSyncAttempts: 600,
minimumTimeBetweenSyncAttempts: 2,
timeBetweenSyncAttemptsAdjustmentFactor: 2,
maximumAllowedDrift: 100,
enableBackgroundSyncAfterSegmentDownloadError: true,
defaultTimingSource: {
scheme: 'urn:mpeg:dash:utc:http-xsdate:2014',
value: 'http://time.akamai.com/?iso&ms'
}
}
}
})