dashjs
Version:
A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers.
917 lines (751 loc) • 41.4 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>dash.js Source: streaming/controllers/TimeSyncController.js</title>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/sunlight.default.css">
<link type="text/css" rel="stylesheet" href="styles/site.spacelab.css">
</head>
<body>
<div class="navbar navbar-default navbar-fixed-top navbar-inverse">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="index.html">dash.js</a>
<button class="navbar-toggle" type="button" data-toggle="collapse" data-target="#topNavigation">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div class="navbar-collapse collapse" id="topNavigation">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="modules.list.html" class="dropdown-toggle" data-toggle="dropdown">Modules<b class="caret"></b></a>
<ul class="dropdown-menu ">
<li><a href="module-DashAdapter.html">DashAdapter</a></li><li><a href="module-DashMetrics.html">DashMetrics</a></li><li><a href="module-MediaPlayer.html">MediaPlayer</a></li><li><a href="module-OfflineController.html">OfflineController</a></li><li><a href="module-ProtectionController.html">ProtectionController</a></li><li><a href="module-Settings.html">Settings</a></li>
</ul>
</li>
<li class="dropdown">
<a href="classes.list.html" class="dropdown-toggle" data-toggle="dropdown">Classes<b class="caret"></b></a>
<ul class="dropdown-menu ">
<li><a href="Errors.html">Errors</a></li><li><a href="MediaPlayerEvents.html">MediaPlayerEvents</a></li><li><a href="MediaPlayerModel.html">MediaPlayerModel</a></li><li><a href="MetricsReportingEvents.html">MetricsReportingEvents</a></li><li><a href="MssErrors.html">MssErrors</a></li><li><a href="OfflineErrors.html">OfflineErrors</a></li><li><a href="OfflineEvents.html">OfflineEvents</a></li><li><a href="ProtectionErrors.html">ProtectionErrors</a></li><li><a href="ProtectionEvents.html">ProtectionEvents</a></li>
</ul>
</li>
<li class="dropdown">
<a href="events.list.html" class="dropdown-toggle" data-toggle="dropdown">Events<b class="caret"></b></a>
<ul class="dropdown-menu ">
<li><a href="MediaPlayerEvents.html#event:ADAPTATION_SET_REMOVED_NO_CAPABILITIES">MediaPlayerEvents#event:ADAPTATION_SET_REMOVED_NO_CAPABILITIES</a></li><li><a href="MediaPlayerEvents.html#event:AST_IN_FUTURE">MediaPlayerEvents#event:AST_IN_FUTURE</a></li><li><a href="MediaPlayerEvents.html#event:BUFFER_EMPTY">MediaPlayerEvents#event:BUFFER_EMPTY</a></li><li><a href="MediaPlayerEvents.html#event:BUFFER_LEVEL_STATE_CHANGED">MediaPlayerEvents#event:BUFFER_LEVEL_STATE_CHANGED</a></li><li><a href="MediaPlayerEvents.html#event:BUFFER_LEVEL_UPDATED">MediaPlayerEvents#event:BUFFER_LEVEL_UPDATED</a></li><li><a href="MediaPlayerEvents.html#event:BUFFER_LOADED">MediaPlayerEvents#event:BUFFER_LOADED</a></li><li><a href="MediaPlayerEvents.html#event:CAN_PLAY">MediaPlayerEvents#event:CAN_PLAY</a></li><li><a href="MediaPlayerEvents.html#event:CAN_PLAY_THROUGH">MediaPlayerEvents#event:CAN_PLAY_THROUGH</a></li><li><a href="MediaPlayerEvents.html#event:CAPTION_CONTAINER_RESIZE">MediaPlayerEvents#event:CAPTION_CONTAINER_RESIZE</a></li><li><a href="MediaPlayerEvents.html#event:CAPTION_RENDERED">MediaPlayerEvents#event:CAPTION_RENDERED</a></li><li><a href="MediaPlayerEvents.html#event:CONFORMANCE_VIOLATION">MediaPlayerEvents#event:CONFORMANCE_VIOLATION</a></li><li><a href="MediaPlayerEvents.html#event:CONTENT_STEERING_REQUEST_COMPLETED">MediaPlayerEvents#event:CONTENT_STEERING_REQUEST_COMPLETED</a></li><li><a href="MediaPlayerEvents.html#event:DYNAMIC_TO_STATIC">MediaPlayerEvents#event:DYNAMIC_TO_STATIC</a></li><li><a href="MediaPlayerEvents.html#event:ERROR">MediaPlayerEvents#event:ERROR</a></li><li><a href="MediaPlayerEvents.html#event:EVENT_MODE_ON_RECEIVE">MediaPlayerEvents#event:EVENT_MODE_ON_RECEIVE</a></li><li><a href="MediaPlayerEvents.html#event:EVENT_MODE_ON_START">MediaPlayerEvents#event:EVENT_MODE_ON_START</a></li><li><a href="MediaPlayerEvents.html#event:FRAGMENT_LOADING_ABANDONED">MediaPlayerEvents#event:FRAGMENT_LOADING_ABANDONED</a></li><li><a href="MediaPlayerEvents.html#event:FRAGMENT_LOADING_COMPLETED">MediaPlayerEvents#event:FRAGMENT_LOADING_COMPLETED</a></li><li><a href="MediaPlayerEvents.html#event:FRAGMENT_LOADING_PROGRESS">MediaPlayerEvents#event:FRAGMENT_LOADING_PROGRESS</a></li><li><a href="MediaPlayerEvents.html#event:FRAGMENT_LOADING_STARTED">MediaPlayerEvents#event:FRAGMENT_LOADING_STARTED</a></li><li><a href="MediaPlayerEvents.html#event:LOG">MediaPlayerEvents#event:LOG</a></li><li><a href="MediaPlayerEvents.html#event:MANIFEST_LOADED">MediaPlayerEvents#event:MANIFEST_LOADED</a></li><li><a href="MediaPlayerEvents.html#event:MANIFEST_VALIDITY_CHANGED">MediaPlayerEvents#event:MANIFEST_VALIDITY_CHANGED</a></li><li><a href="MediaPlayerEvents.html#event:METRIC_ADDED">MediaPlayerEvents#event:METRIC_ADDED</a></li><li><a href="MediaPlayerEvents.html#event:METRIC_CHANGED">MediaPlayerEvents#event:METRIC_CHANGED</a></li><li><a href="MediaPlayerEvents.html#event:METRIC_UPDATED">MediaPlayerEvents#event:METRIC_UPDATED</a></li><li><a href="MediaPlayerEvents.html#event:METRICS_CHANGED">MediaPlayerEvents#event:METRICS_CHANGED</a></li><li><a href="MediaPlayerEvents.html#event:PERIOD_SWITCH_COMPLETED">MediaPlayerEvents#event:PERIOD_SWITCH_COMPLETED</a></li><li><a href="MediaPlayerEvents.html#event:PERIOD_SWITCH_STARTED">MediaPlayerEvents#event:PERIOD_SWITCH_STARTED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_ENDED">MediaPlayerEvents#event:PLAYBACK_ENDED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_ERROR">MediaPlayerEvents#event:PLAYBACK_ERROR</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_LOADED_DATA">MediaPlayerEvents#event:PLAYBACK_LOADED_DATA</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_METADATA_LOADED">MediaPlayerEvents#event:PLAYBACK_METADATA_LOADED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_NOT_ALLOWED">MediaPlayerEvents#event:PLAYBACK_NOT_ALLOWED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_PAUSED">MediaPlayerEvents#event:PLAYBACK_PAUSED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_PLAYING">MediaPlayerEvents#event:PLAYBACK_PLAYING</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_PROGRESS">MediaPlayerEvents#event:PLAYBACK_PROGRESS</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_RATE_CHANGED">MediaPlayerEvents#event:PLAYBACK_RATE_CHANGED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_SEEKED">MediaPlayerEvents#event:PLAYBACK_SEEKED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_SEEKING">MediaPlayerEvents#event:PLAYBACK_SEEKING</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_STALLED">MediaPlayerEvents#event:PLAYBACK_STALLED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_STARTED">MediaPlayerEvents#event:PLAYBACK_STARTED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_TIME_UPDATED">MediaPlayerEvents#event:PLAYBACK_TIME_UPDATED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_VOLUME_CHANGED">MediaPlayerEvents#event:PLAYBACK_VOLUME_CHANGED</a></li><li><a href="MediaPlayerEvents.html#event:PLAYBACK_WAITING">MediaPlayerEvents#event:PLAYBACK_WAITING</a></li><li><a href="MediaPlayerEvents.html#event:QUALITY_CHANGE_RENDERED">MediaPlayerEvents#event:QUALITY_CHANGE_RENDERED</a></li><li><a href="MediaPlayerEvents.html#event:QUALITY_CHANGE_REQUESTED">MediaPlayerEvents#event:QUALITY_CHANGE_REQUESTED</a></li><li><a href="MediaPlayerEvents.html#event:REPRESENTATION_SWITCH">MediaPlayerEvents#event:REPRESENTATION_SWITCH</a></li><li><a href="MediaPlayerEvents.html#event:STREAM_ACTIVATED">MediaPlayerEvents#event:STREAM_ACTIVATED</a></li><li><a href="MediaPlayerEvents.html#event:STREAM_DEACTIVATED">MediaPlayerEvents#event:STREAM_DEACTIVATED</a></li><li><a href="MediaPlayerEvents.html#event:STREAM_INITIALIZED">MediaPlayerEvents#event:STREAM_INITIALIZED</a></li><li><a href="MediaPlayerEvents.html#event:STREAM_INITIALIZING">MediaPlayerEvents#event:STREAM_INITIALIZING</a></li><li><a href="MediaPlayerEvents.html#event:STREAM_TEARDOWN_COMPLETE">MediaPlayerEvents#event:STREAM_TEARDOWN_COMPLETE</a></li><li><a href="MediaPlayerEvents.html#event:STREAM_UPDATED">MediaPlayerEvents#event:STREAM_UPDATED</a></li><li><a href="MediaPlayerEvents.html#event:TEXT_TRACK_ADDED">MediaPlayerEvents#event:TEXT_TRACK_ADDED</a></li><li><a href="MediaPlayerEvents.html#event:TEXT_TRACKS_ADDED">MediaPlayerEvents#event:TEXT_TRACKS_ADDED</a></li><li><a href="MediaPlayerEvents.html#event:TRACK_CHANGE_RENDERED">MediaPlayerEvents#event:TRACK_CHANGE_RENDERED</a></li><li><a href="MediaPlayerEvents.html#event:TTML_PARSED">MediaPlayerEvents#event:TTML_PARSED</a></li><li><a href="MediaPlayerEvents.html#event:TTML_TO_PARSE">MediaPlayerEvents#event:TTML_TO_PARSE</a></li><li><a href="MetricsReportingEvents.html#event:CMCD_DATA_GENERATED">MetricsReportingEvents#event:CMCD_DATA_GENERATED</a></li><li><a href="OfflineEvents.html#event:OFFLINE_RECORD_FINISHED">OfflineEvents#event:OFFLINE_RECORD_FINISHED</a></li><li><a href="OfflineEvents.html#event:OFFLINE_RECORD_LOADEDMETADATA">OfflineEvents#event:OFFLINE_RECORD_LOADEDMETADATA</a></li><li><a href="OfflineEvents.html#event:OFFLINE_RECORD_STARTED">OfflineEvents#event:OFFLINE_RECORD_STARTED</a></li><li><a href="OfflineEvents.html#event:OFFLINE_RECORD_STOPPED">OfflineEvents#event:OFFLINE_RECORD_STOPPED</a></li><li><a href="ProtectionEvents.html#event:KEY_ADDED">ProtectionEvents#event:KEY_ADDED</a></li><li><a href="ProtectionEvents.html#event:KEY_ERROR">ProtectionEvents#event:KEY_ERROR</a></li><li><a href="ProtectionEvents.html#event:KEY_MESSAGE">ProtectionEvents#event:KEY_MESSAGE</a></li><li><a href="ProtectionEvents.html#event:KEY_SESSION_CLOSED">ProtectionEvents#event:KEY_SESSION_CLOSED</a></li><li><a href="ProtectionEvents.html#event:KEY_SESSION_CREATED">ProtectionEvents#event:KEY_SESSION_CREATED</a></li><li><a href="ProtectionEvents.html#event:KEY_SESSION_REMOVED">ProtectionEvents#event:KEY_SESSION_REMOVED</a></li><li><a href="ProtectionEvents.html#event:KEY_STATUSES_CHANGED">ProtectionEvents#event:KEY_STATUSES_CHANGED</a></li><li><a href="ProtectionEvents.html#event:KEY_SYSTEM_SELECTED">ProtectionEvents#event:KEY_SYSTEM_SELECTED</a></li><li><a href="ProtectionEvents.html#event:LICENSE_REQUEST_COMPLETE">ProtectionEvents#event:LICENSE_REQUEST_COMPLETE</a></li><li><a href="ProtectionEvents.html#event:LICENSE_REQUEST_SENDING">ProtectionEvents#event:LICENSE_REQUEST_SENDING</a></li><li><a href="ProtectionEvents.html#event:PROTECTION_CREATED">ProtectionEvents#event:PROTECTION_CREATED</a></li><li><a href="ProtectionEvents.html#event:PROTECTION_DESTROYED">ProtectionEvents#event:PROTECTION_DESTROYED</a></li>
</ul>
</li>
<li class="dropdown">
<a href="global.html" class="dropdown-toggle" data-toggle="dropdown">Global<b class="caret"></b></a>
<ul class="dropdown-menu ">
<li><a href="global.html#LICENSE_SERVER_MANIFEST_CONFIGURATIONS">LICENSE_SERVER_MANIFEST_CONFIGURATIONS</a></li>
</ul>
</li>
</ul>
<div class="col-sm-3 col-md-3">
<form class="navbar-form" role="search">
<div class="input-group">
<input type="text" class="form-control" placeholder="Search" name="q" id="search-input">
<div class="input-group-btn">
<button class="btn btn-default" id="search-submit"><i class="glyphicon glyphicon-search"></i></button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="container" id="toc-content">
<div class="row">
<div class="col-md-12">
<div id="main">
<h1 class="page-title">Source: streaming/controllers/TimeSyncController.js</h1>
<section>
<article>
<pre
class="sunlight-highlight-javascript linenums">/**
* 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.
*/
import DashJSError from './../vo/DashJSError';
import {HTTPRequest} from '../vo/metrics/HTTPRequest';
import EventBus from './../../core/EventBus';
import Events from './../../core/events/Events';
import Errors from './../../core/errors/Errors';
import FactoryMaker from '../../core/FactoryMaker';
import Debug from '../../core/Debug';
import URLUtils from '../utils/URLUtils';
const HTTP_TIMEOUT_MS = 5000;
const DEFAULT_MAXIMUM_ALLOWED_DRIFT = 100;
const DEFAULT_TIME_BETWEEN_SYNC_ATTEMPTS_ADJUSTMENT_FACTOR = 2;
const DEFAULT_BACKGROUND_ATTEMPTS = 2;
const DEFAULT_TIME_BETWEEN_SYNC_ATTEMPTS = 30;
const DEFAULT_MINIMUM_TIME_BETWEEN_BACKGROUND_SYNC_ATTEMPTS = 30;
const DEFAULT_MAXIMUM_TIME_BETWEEN_SYNC = 600;
const DEFAULT_MINIMUM_TIME_BETWEEN_SYNC = 2;
function TimeSyncController() {
const context = this.context;
const eventBus = EventBus(context).getInstance();
const urlUtils = URLUtils(context).getInstance();
let instance,
logger,
isSynchronizing,
isBackgroundSynchronizing,
settings,
handlers,
dashMetrics,
backgroundSyncTimeOffsets,
timingSources,
timeOfLastSync,
timeOfLastBackgroundSync,
lastOffset,
lastTimingSource,
internalTimeBetweenSyncAttempts,
errHandler,
baseURLController;
function setup() {
logger = Debug(context).getInstance().getLogger(instance);
eventBus.on(Events.ATTEMPT_BACKGROUND_SYNC, _onAttemptBackgroundSync, instance);
}
function setConfig(config) {
if (!config) return;
if (config.dashMetrics) {
dashMetrics = config.dashMetrics;
}
if (config.baseURLController) {
baseURLController = config.baseURLController;
}
if (config.errHandler) {
errHandler = config.errHandler;
}
if (config.settings) {
settings = config.settings;
}
}
function _resetInitialSettings() {
backgroundSyncTimeOffsets = [];
timingSources = [];
timeOfLastSync = null;
timeOfLastBackgroundSync = null;
lastTimingSource = null;
lastOffset = NaN;
isSynchronizing = false;
isBackgroundSynchronizing = false;
internalTimeBetweenSyncAttempts = settings.get().streaming.utcSynchronization.timeBetweenSyncAttempts;
}
/**
* Register the timing handler depending on the schemeIdUris. This method is called once when the StreamController is initialized
*/
function initialize() {
_resetInitialSettings();
// a list of known schemeIdUris and a method to call with @value
handlers = {
'urn:mpeg:dash:utc:http-head:2014': _httpHeadHandler,
'urn:mpeg:dash:utc:http-xsdate:2014': _httpHandler.bind(null, _xsdatetimeDecoder),
'urn:mpeg:dash:utc:http-iso:2014': _httpHandler.bind(null, _iso8601Decoder),
'urn:mpeg:dash:utc:direct:2014': _directHandler,
// some specs referencing early ISO23009-1 drafts incorrectly use
// 2012 in the URI, rather than 2014. support these for now.
'urn:mpeg:dash:utc:http-head:2012': _httpHeadHandler,
'urn:mpeg:dash:utc:http-xsdate:2012': _httpHandler.bind(null, _xsdatetimeDecoder),
'urn:mpeg:dash:utc:http-iso:2012': _httpHandler.bind(null, _iso8601Decoder),
'urn:mpeg:dash:utc:direct:2012': _directHandler,
// it isn't clear how the data returned would be formatted, and
// no public examples available so http-ntp not supported for now.
// presumably you would do an arraybuffer type xhr and decode the
// binary data returned but I would want to see a sample first.
'urn:mpeg:dash:utc:http-ntp:2014': _notSupportedHandler,
// not clear how this would be supported in javascript (in browser)
'urn:mpeg:dash:utc:ntp:2014': _notSupportedHandler,
'urn:mpeg:dash:utc:sntp:2014': _notSupportedHandler
};
}
/**
* Sync against a timing source. T
* @param {array} tSources
* @param {boolean} isDynamic
*/
function attemptSync(tSources, isDynamic) {
timingSources = tSources;
// Stop if we are already synchronizing
if (isSynchronizing) {
return;
}
// No synchronization required we can signal the completion immediately
if (!_shouldPerformSynchronization(isDynamic)) {
eventBus.trigger(Events.TIME_SYNCHRONIZATION_COMPLETED);
return;
}
isSynchronizing = true;
_attemptRecursiveSync();
}
/**
* Does a synchronization in the background in case the last offset should be verified or a 404 occurs
*/
function _onAttemptBackgroundSync() {
if (!settings.get().streaming.utcSynchronization.enabled || isSynchronizing || isBackgroundSynchronizing || !lastTimingSource || !lastTimingSource.value || !lastTimingSource.schemeIdUri || isNaN(lastOffset) || isNaN(settings.get().streaming.utcSynchronization.backgroundAttempts)) {
return;
}
if (timeOfLastBackgroundSync && ((Date.now() - timeOfLastBackgroundSync) / 1000) < DEFAULT_MINIMUM_TIME_BETWEEN_BACKGROUND_SYNC_ATTEMPTS) {
return;
}
backgroundSyncTimeOffsets = [];
isBackgroundSynchronizing = true;
const backgroundAttempts = !isNaN(settings.get().streaming.utcSynchronization.backgroundAttempts) ? settings.get().streaming.utcSynchronization.backgroundAttempts : DEFAULT_BACKGROUND_ATTEMPTS;
_attemptBackgroundSync(backgroundAttempts);
}
/**
* Perform a defined number of background attempts
* @param {number} attempts
* @private
*/
function _attemptBackgroundSync(attempts) {
try {
if (attempts <= 0) {
_completeBackgroundTimeSyncSequence();
return;
}
const deviceTimeBeforeSync = Date.now();
handlers[lastTimingSource.schemeIdUri](
lastTimingSource.value,
function (serverTime) {
// the timing source returned something useful
const deviceTimeAfterSync = Date.now();
const offset = _calculateOffset(deviceTimeBeforeSync, deviceTimeAfterSync, serverTime);
backgroundSyncTimeOffsets.push(offset);
_attemptBackgroundSync(attempts - 1);
},
function () {
_completeBackgroundTimeSyncSequence();
}
);
} catch (e) {
_completeBackgroundTimeSyncSequence();
}
}
/**
* Sync against a timing source. This method is called recursively if the time sync for the first entry in timingSources fails.
* @param {number} sourceIndex
*/
function _attemptRecursiveSync(sourceIndex = null) {
// if called with no sourceIndex, use zero (highest priority)
let index = sourceIndex || 0;
// the sources should be ordered in priority from the manifest.
// try each in turn, from the top, until either something
// sensible happens, or we run out of sources to try.
if (!timingSources || timingSources.length === 0 || index >= timingSources.length) {
_onComplete();
return;
}
let source = timingSources[index];
if (source) {
// check if there is a handler for this @schemeIdUri
if (handlers.hasOwnProperty(source.schemeIdUri)) {
// if so, call it with its @value
const deviceTimeBeforeSync = new Date().getTime();
handlers[source.schemeIdUri](
source.value,
function (serverTime) {
// the timing source returned something useful
const deviceTimeAfterSync = new Date().getTime();
const offset = _calculateOffset(deviceTimeBeforeSync, deviceTimeAfterSync, serverTime);
lastTimingSource = source;
_onComplete(offset);
},
function () {
// the timing source was probably uncontactable
// or returned something we can't use - try again
// with the remaining sources
_attemptRecursiveSync(index + 1);
}
);
} else {
// an unknown schemeIdUri must have been found
// try again with the remaining sources
_attemptRecursiveSync(index + 1);
}
} else {
// no valid time source could be found, just use device time
_onComplete();
}
}
/**
* Calculate the offset between client and server. Account for the roundtrip time
* @param {number} deviceTimeBeforeSync
* @param {number} deviceTimeAfterSync
* @param {number} serverTime
* @return {number}
* @private
*/
function _calculateOffset(deviceTimeBeforeSync, deviceTimeAfterSync, serverTime) {
const deviceReferenceTime = deviceTimeAfterSync - ((deviceTimeAfterSync - deviceTimeBeforeSync) / 2);
return serverTime - deviceReferenceTime;
}
/**
* Checks if a synchronization is required
* @param {boolean} isDynamic
* @return {boolean}
* @private
*/
function _shouldPerformSynchronization(isDynamic) {
try {
if (!isDynamic || !settings.get().streaming.utcSynchronization.enabled) {
return false;
}
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;
}
}
/**
* Callback after sync has been completed
* @param {number} offset
* @private
*/
function _onComplete(offset = NaN) {
let failed = isNaN(offset);
if (failed && settings.get().streaming.utcSynchronization.useManifestDateHeaderTimeSource) {
//Before falling back to binary search , check if date header exists on MPD. if so, use for a time source.
_checkForDateHeader();
} else {
_completeTimeSyncSequence(failed, offset);
}
}
/**
* Takes xsdatetime and returns milliseconds since UNIX epoch. May not be necessary as xsdatetime is very similar to ISO 8601 which is natively understood by javascript Date parser
* @param {string} xsdatetimeStr
* @return {number}
* @private
*/
function _alternateXsdatetimeDecoder(xsdatetimeStr) {
// taken from DashParser - should probably refactor both uses
const SECONDS_IN_MIN = 60;
const MINUTES_IN_HOUR = 60;
const MILLISECONDS_IN_SECONDS = 1000;
let datetimeRegex = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2})(?::([0-9]*)(\.[0-9]*)?)?(?:([+\-])([0-9]{2})([0-9]{2}))?/;
let utcDate,
timezoneOffset;
let match = datetimeRegex.exec(xsdatetimeStr);
// If the string does not contain a timezone offset different browsers can interpret it either
// as UTC or as a local time so we have to parse the string manually to normalize the given date value for
// all browsers
utcDate = Date.UTC(
parseInt(match[1], 10),
parseInt(match[2], 10) - 1, // months start from zero
parseInt(match[3], 10),
parseInt(match[4], 10),
parseInt(match[5], 10),
(match[6] && (parseInt(match[6], 10) || 0)),
(match[7] && parseFloat(match[7]) * MILLISECONDS_IN_SECONDS) || 0
);
// If the date has timezone offset take it into account as well
if (match[9] && match[10]) {
timezoneOffset = parseInt(match[9], 10) * MINUTES_IN_HOUR + parseInt(match[10], 10);
utcDate += (match[8] === '+' ? -1 : +1) * timezoneOffset * SECONDS_IN_MIN * MILLISECONDS_IN_SECONDS;
}
return new Date(utcDate).getTime();
}
/**
* Try to use the built in parser, since xsdate is a constrained ISO8601 which is supported natively by Date.parse. if that fails, try a regex-based version used elsewhere in this application.
* @param {string} xsdatetimeStr
* @return {number}
*/
function _xsdatetimeDecoder(xsdatetimeStr) {
let parsedDate = Date.parse(xsdatetimeStr);
if (isNaN(parsedDate)) {
parsedDate = _alternateXsdatetimeDecoder(xsdatetimeStr);
}
return parsedDate;
}
/**
* Takes ISO 8601 timestamp and returns milliseconds since UNIX epoch
* @param {string} isoStr
* @return {number}
*/
function _iso8601Decoder(isoStr) {
return Date.parse(isoStr);
}
/**
* Takes RFC 1123 timestamp (which is same as ISO8601) and returns milliseconds since UNIX epoch
* @param {string} dateStr
* @return {number}
*/
function _rfc1123Decoder(dateStr) {
return Date.parse(dateStr);
}
/**
* Handler for unsupported scheme ids.
* @param {string} url
* @param {function} onSuccessCB
* @param {function} onFailureCB
* @private
*/
function _notSupportedHandler(url, onSuccessCB, onFailureCB) {
onFailureCB();
}
/**
* Direct handler
* @param {string} xsdatetimeStr
* @param {function} onSuccessCB
* @param {function} onFailureCB
*/
function _directHandler(xsdatetimeStr, onSuccessCB, onFailureCB) {
let time = _xsdatetimeDecoder(xsdatetimeStr);
if (!isNaN(time)) {
onSuccessCB(time);
return;
}
onFailureCB();
}
/**
* Generic http handler
* @param {function} decoder
* @param {string} url
* @param {function} onSuccessCB
* @param {function} onFailureCB
* @param {boolean} isHeadRequest
* @private
*/
function _httpHandler(decoder, url, onSuccessCB, onFailureCB, isHeadRequest) {
let oncomplete,
onload;
let complete = false;
let req = new XMLHttpRequest();
let verb = isHeadRequest ? HTTPRequest.HEAD : HTTPRequest.GET;
let urls = url.match(/\S+/g);
// according to ISO 23009-1, url could be a white-space
// separated list of URLs. just handle one at a time.
url = urls.shift();
oncomplete = function () {
if (complete) {
return;
}
// we only want to pass through here once per xhr,
// regardless of whether the load was successful.
complete = true;
// if there are more urls to try, call self.
if (urls.length) {
_httpHandler(decoder, urls.join(' '), onSuccessCB, onFailureCB, isHeadRequest);
} else {
onFailureCB();
}
};
onload = function () {
let time,
result;
if (req.status === 200) {
time = isHeadRequest ?
req.getResponseHeader('Date') :
req.response;
result = decoder(time);
// decoder returns NaN if non-standard input
if (!isNaN(result)) {
onSuccessCB(result);
complete = true;
}
}
};
if (urlUtils.isRelative(url)) {
// passing no path to resolve will return just MPD BaseURL/baseUri
const baseUrl = baseURLController.resolve();
if (baseUrl) {
url = urlUtils.resolve(url, baseUrl.url);
}
}
req.open(verb, url);
req.timeout = HTTP_TIMEOUT_MS || 0;
req.onload = onload;
req.onloadend = oncomplete;
req.send();
}
/**
* Handler for http-head schemeIdUri
* @param {string} url
* @param {function} onSuccessCB
* @param {function} onFailureCB
* @private
*/
function _httpHeadHandler(url, onSuccessCB, onFailureCB) {
_httpHandler(_rfc1123Decoder, url, onSuccessCB, onFailureCB, true);
}
/**
* Checks if a date header is present in the MPD response and calculates the offset based on the header
* @private
*/
function _checkForDateHeader() {
let dateHeaderValue = dashMetrics.getLatestMPDRequestHeaderValueByID('Date');
let dateHeaderTime = dateHeaderValue !== null ? new Date(dateHeaderValue).getTime() : Number.NaN;
if (!isNaN(dateHeaderTime)) {
const offsetToDeviceTimeMs = dateHeaderTime - Date.now();
_completeTimeSyncSequence(false, offsetToDeviceTimeMs);
} else {
_completeTimeSyncSequence(true);
}
}
/**
* Triggers the event to signal that the time synchronization was completed
* @param {boolean} failed
* @param {number} offset
* @private
*/
function _completeTimeSyncSequence(failed, offset) {
// Adjust the time of the next sync based on the drift between current offset and last offset
if (!isNaN(lastOffset) && !isNaN(offset) && !failed) {
_adjustTimeBetweenSyncAttempts(offset);
}
// Update the internal data
if (!failed && !isNaN(offset)) {
timeOfLastSync = Date.now();
isSynchronizing = false;
// if this is the first sync we are doing perform background syncs as well to confirm current offset
const shouldAttemptBackgroundSync = isNaN(lastOffset);
lastOffset = offset;
if (shouldAttemptBackgroundSync) {
_onAttemptBackgroundSync();
}
logger.debug(`Completed UTC sync. Setting client - server offset to ${offset}`);
}
if (failed) {
lastTimingSource = null;
isSynchronizing = false;
errHandler.error(new DashJSError(Errors.TIME_SYNC_FAILED_ERROR_CODE, Errors.TIME_SYNC_FAILED_ERROR_MESSAGE));
}
// Notify other classes
eventBus.trigger(Events.UPDATE_TIME_SYNC_OFFSET, {
offset: offset,
});
eventBus.trigger(Events.TIME_SYNCHRONIZATION_COMPLETED);
}
function _adjustTimeBetweenSyncAttempts(offset) {
try {
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;
} catch (e) {
}
}
/**
* Callback after all background syncs have been completed.
* @private
*/
function _completeBackgroundTimeSyncSequence() {
if (!backgroundSyncTimeOffsets || backgroundSyncTimeOffsets.length === 0) {
return;
}
const averageOffset = backgroundSyncTimeOffsets.reduce((acc, curr) => {
return acc + curr;
}, 0) / backgroundSyncTimeOffsets.length;
if (!_isOffsetDriftWithinThreshold(averageOffset)) {
logger.debug(`Completed background UTC sync. Setting client - server offset to ${averageOffset}`);
lastOffset = averageOffset;
eventBus.trigger(Events.UPDATE_TIME_SYNC_OFFSET, {
offset: lastOffset
});
} else {
logger.debug(`Completed background UTC sync. Offset is within allowed threshold and is not adjusted.`);
}
isBackgroundSynchronizing = false;
timeOfLastBackgroundSync = Date.now();
}
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;
}
}
function reset() {
_resetInitialSettings();
eventBus.off(Events.ATTEMPT_BACKGROUND_SYNC, _onAttemptBackgroundSync, instance);
}
instance = {
initialize,
attemptSync,
setConfig,
reset
};
setup();
return instance;
}
TimeSyncController.__dashjs_factory_name = 'TimeSyncController';
const factory = FactoryMaker.getSingletonFactory(TimeSyncController);
factory.HTTP_TIMEOUT_MS = HTTP_TIMEOUT_MS;
FactoryMaker.updateSingletonFactory(TimeSyncController.__dashjs_factory_name, factory);
export default factory;
</pre>
</article>
</section>
</div>
</div>
<div class="clearfix"></div>
</div>
</div>
<div class="modal fade" id="searchResults">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
<h4 class="modal-title">Search results</h4>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
<footer>
<span class="copyright">
<h3>DASH Industry Forum</h3>
</span>
<span class="jsdoc-message">
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.6.7</a>
on Wed Sep 28th 2022
using the <a href="https://github.com/docstrap/docstrap">DocStrap template</a>.
</span>
</footer>
<script src="scripts/docstrap.lib.js"></script>
<script src="scripts/toc.js"></script>
<script type="text/javascript" src="scripts/fulltext-search-ui.js"></script>
<script>
$( function () {
$( "[id*='$']" ).each( function () {
var $this = $( this );
$this.attr( "id", $this.attr( "id" ).replace( "$", "__" ) );
} );
$( ".tutorial-section pre, .readme-section pre, pre.prettyprint.source" ).each( function () {
var $this = $( this );
var example = $this.find( "code" );
exampleText = example.html();
var lang = /{@lang (.*?)}/.exec( exampleText );
if ( lang && lang[1] ) {
exampleText = exampleText.replace( lang[0], "" );
example.html( exampleText );
lang = lang[1];
} else {
var langClassMatch = example.parent()[0].className.match(/lang\-(\S+)/);
lang = langClassMatch ? langClassMatch[1] : "javascript";
}
if ( lang ) {
$this
.addClass( "sunlight-highlight-" + lang )
.addClass( "linenums" )
.html( example.html() );
}
} );
Sunlight.highlightAll( {
lineNumbers : true,
showMenu : true,
enableDoclinks : true
} );
$.catchAnchorLinks( {
navbarOffset: 10
} );
$( "#toc" ).toc( {
anchorName : function ( i, heading, prefix ) {
return $( heading ).attr( "id" ) || ( prefix + i );
},
selectors : "#toc-content h1,#toc-content h2,#toc-content h3,#toc-content h4",
showAndHide : false,
smoothScrolling: true
} );
$( "#main span[id^='toc']" ).addClass( "toc-shim" );
$( '.dropdown-toggle' ).dropdown();
$( "table" ).each( function () {
var $this = $( this );
$this.addClass('table');
} );
} );
</script>
<!--Navigation and Symbol Display-->
<script>
$( function () {
$( '#main' ).localScroll( {
offset : { top : 60 } //offset by the height of your header (give or take a few px, see what works for you)
} );
$( "dt.name" ).each( function () {
var $this = $( this ).find("h4");
var icon = $( "<i/>" ).addClass( "icon-plus-sign" ).addClass( "pull-right" ).addClass( "icon-white" );
var dt = $(this);
var children = dt.next( "dd" );
dt.prepend( icon ).css( {cursor : "pointer"} );
dt.addClass( "member-collapsed" ).addClass( "member" );
children.hide();
dt.children().on( "click", function () {
children = dt.next( "dd" );
children.slideToggle( "fast", function () {
if ( children.is( ":visible" ) ) {
icon.addClass( "icon-minus-sign" ).removeClass( "icon-plus-sign" ).removeClass( "icon-white" );
dt.addClass( "member-open" ).animate( "member-collapsed" );
} else {
icon.addClass( "icon-plus-sign" ).removeClass( "icon-minus-sign" ).addClass( "icon-white" );
dt.addClass( "member-collapsed" ).removeClass( "member-open" );
}
} );
} );
} );
} );
</script>
<!--Google Analytics-->
<script type="text/javascript">
$(document).ready(function() {
SearcherDisplay.init();
});
</script>
</body>
</html>