UNPKG

@l5i/dashjs

Version:

A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers.

398 lines (330 loc) 14.8 kB
/** * 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 Constants from '../constants/Constants'; 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; function TimeSyncController() { const context = this.context; const eventBus = EventBus(context).getInstance(); const urlUtils = URLUtils(context).getInstance(); let instance, logger, offsetToDeviceTimeMs, isSynchronizing, isInitialised, useManifestDateHeaderTimeSource, handlers, metricsModel, dashMetrics, baseURLController; function setup() { logger = Debug(context).getInstance().getLogger(instance); } function initialize(timingSources, useManifestDateHeader) { useManifestDateHeaderTimeSource = useManifestDateHeader; offsetToDeviceTimeMs = 0; isSynchronizing = false; isInitialised = false; // 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 }; if (!getIsSynchronizing()) { attemptSync(timingSources); setIsInitialised(true); } } function setConfig(config) { if (!config) return; if (config.metricsModel) { metricsModel = config.metricsModel; } if (config.dashMetrics) { dashMetrics = config.dashMetrics; } if (config.baseURLController) { baseURLController = config.baseURLController; } } function getOffsetToDeviceTimeMs() { return getOffsetMs(); } function setIsSynchronizing(value) { isSynchronizing = value; } function getIsSynchronizing() { return isSynchronizing; } function setIsInitialised(value) { isInitialised = value; } function setOffsetMs(value) { offsetToDeviceTimeMs = value; } function getOffsetMs() { return offsetToDeviceTimeMs; } // 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 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. 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 function iso8601Decoder(isoStr) { return Date.parse(isoStr); } // takes RFC 1123 timestamp (which is same as ISO8601) and returns // milliseconds since UNIX epoch function rfc1123Decoder(dateStr) { return Date.parse(dateStr); } function notSupportedHandler(url, onSuccessCB, onFailureCB) { onFailureCB(); } function directHandler(xsdatetimeStr, onSuccessCB, onFailureCB) { let time = xsdatetimeDecoder(xsdatetimeStr); if (!isNaN(time)) { onSuccessCB(time); return; } onFailureCB(); } 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(); } function httpHeadHandler(url, onSuccessCB, onFailureCB) { httpHandler(rfc1123Decoder, url, onSuccessCB, onFailureCB, true); } function checkForDateHeader() { let metrics = metricsModel.getReadOnlyMetricsFor(Constants.STREAM); let dateHeaderValue = dashMetrics.getLatestMPDRequestHeaderValueByID(metrics, 'Date'); let dateHeaderTime = dateHeaderValue !== null ? new Date(dateHeaderValue).getTime() : Number.NaN; if (!isNaN(dateHeaderTime)) { setOffsetMs(dateHeaderTime - new Date().getTime()); completeTimeSyncSequence(false, dateHeaderTime / 1000, offsetToDeviceTimeMs); } else { completeTimeSyncSequence(true); } } function completeTimeSyncSequence(failed, time, offset) { setIsSynchronizing(false); eventBus.trigger(Events.TIME_SYNCHRONIZATION_COMPLETED, { time: time, offset: offset, error: failed ? new DashJSError(Errors.TIME_SYNC_FAILED_ERROR_CODE, Errors.TIME_SYNC_FAILED_ERROR_MESSAGE) : null }); } function calculateTimeOffset(serverTime, deviceTime) { const v = (serverTime - deviceTime) / 1000; let offset; // Math.trunc not implemented by IE11 if (Math.trunc) { offset = Math.trunc(v); } else { offset = (v - v % 1) || (!isFinite(v) || v === 0 ? v : v < 0 ? -0 : 0); } return offset * 1000; } function attemptSync(sources, sourceIndex) { // 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. let source = sources[index]; // callback to emit event to listeners const onComplete = function (time, offset) { let failed = !time || !offset; if (failed && 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, time, offset); } }; setIsSynchronizing(true); if (source) { // check if there is a handler for this @schemeIdUri if (handlers.hasOwnProperty(source.schemeIdUri)) { // if so, call it with its @value handlers[source.schemeIdUri]( source.value, function (serverTime) { // the timing source returned something useful const deviceTime = new Date().getTime(); const offset = calculateTimeOffset(serverTime, deviceTime); setOffsetMs(offset); logger.debug('Local time: ' + new Date(deviceTime)); logger.debug('Server time: ' + new Date(serverTime)); logger.info('Server Time - Local Time (ms): ' + offset); onComplete(serverTime, offset); }, function () { // the timing source was probably uncontactable // or returned something we can't use - try again // with the remaining sources attemptSync(sources, index + 1); } ); } else { // an unknown schemeIdUri must have been found // try again with the remaining sources attemptSync(sources, index + 1); } } else { // no valid time source could be found, just use device time setOffsetMs(0); onComplete(); } } function reset() { setIsInitialised(false); setIsSynchronizing(false); } instance = { initialize: initialize, getOffsetToDeviceTimeMs: getOffsetToDeviceTimeMs, setConfig: setConfig, reset: 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;