/**
* 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 FactoryMaker from '../../core/FactoryMaker';
import Debug from '../../core/Debug';
import URLLoader from '../../streaming/net/URLLoader';
import Errors from '../../core/errors/Errors';
import ContentSteeringRequest from '../vo/ContentSteeringRequest';
import ContentSteeringResponse from '../vo/ContentSteeringResponse';
import DashConstants from '../constants/DashConstants';
import MediaPlayerEvents from '../../streaming/MediaPlayerEvents';
import URLUtils from '../../streaming/utils/URLUtils';
import BaseURL from '../vo/BaseURL';
import MpdLocation from '../vo/MpdLocation';
import Utils from '../../core/Utils.js';
const QUERY_PARAMETER_KEYS = {
THROUGHPUT: '_DASH_throughput',
PATHWAY: '_DASH_pathway',
URL: 'url'
};
const THROUGHPUT_SAMPLES = 4;
function ContentSteeringController() {
const context = this.context;
const urlUtils = URLUtils(context).getInstance();
let instance,
logger,
currentSteeringResponseData,
serviceLocationList,
throughputList,
nextRequestTimer,
urlLoader,
errHandler,
dashMetrics,
mediaPlayerModel,
manifestModel,
requestModifier,
serviceDescriptionController,
eventBus,
adapter;
function setup() {
logger = Debug(context).getInstance().getLogger(instance);
_resetInitialSettings();
}
function setConfig(config) {
if (!config) return;
if (config.adapter) {
adapter = config.adapter;
}
if (config.errHandler) {
errHandler = config.errHandler;
}
if (config.dashMetrics) {
dashMetrics = config.dashMetrics;
}
if (config.mediaPlayerModel) {
mediaPlayerModel = config.mediaPlayerModel;
}
if (config.requestModifier) {
requestModifier = config.requestModifier;
}
if (config.manifestModel) {
manifestModel = config.manifestModel;
}
if (config.serviceDescriptionController) {
serviceDescriptionController = config.serviceDescriptionController;
}
if (config.eventBus) {
eventBus = config.eventBus;
}
}
/**
* Initialize the steering controller by instantiating classes and registering observer callback
*/
function initialize() {
urlLoader = URLLoader(context).create({
errHandler,
dashMetrics,
mediaPlayerModel,
requestModifier,
errors: Errors
});
eventBus.on(MediaPlayerEvents.FRAGMENT_LOADING_STARTED, _onFragmentLoadingStarted, instance);
eventBus.on(MediaPlayerEvents.MANIFEST_LOADING_STARTED, _onManifestLoadingStarted, instance);
eventBus.on(MediaPlayerEvents.MANIFEST_LOADING_FINISHED, _onManifestLoadingFinished, instance);
eventBus.on(MediaPlayerEvents.THROUGHPUT_MEASUREMENT_STORED, _onThroughputMeasurementStored, instance);
}
/**
* When loading of a fragment starts we store its serviceLocation in our list
* @param {object} e
* @private
*/
function _onFragmentLoadingStarted(e) {
_addToServiceLocationList(e, 'baseUrl');
}
/**
* When loading of a manifest starts we store its serviceLocation in our list
* @param {object} e
* @private
*/
function _onManifestLoadingStarted(e) {
_addToServiceLocationList(e, 'location')
}
/**
* Basic throughput calculation for manifest requests
* @param {object} e
* @private
*/
function _onManifestLoadingFinished(e) {
if (!e || !e.request || !e.request.serviceLocation || !e.request.requestStartDate || !e.request.requestEndDate || isNaN(e.request.bytesTotal)) {
return;
}
const serviceLocation = e.request.serviceLocation;
const elapsedTime = e.request.requestEndDate.getTime() - e.request.requestStartDate.getTime();
const throughput = parseInt((((e.request.bytesTotal * 8) / elapsedTime) * 1000)) // bit/s
_storeThroughputForServiceLocation(serviceLocation, throughput);
}
/**
* When a throughput measurement for fragments was stored in ThroughputHistory we save it as well
* @param {object} e
* @private
*/
function _onThroughputMeasurementStored(e) {
if (!e || !e.httpRequest || !e.httpRequest._serviceLocation || isNaN(e.throughput)) {
return;
}
const serviceLocation = e.httpRequest._serviceLocation;
const throughput = e.throughput * 1000;
_storeThroughputForServiceLocation(serviceLocation, throughput);
}
/**
* Helper function to store a throughput value from the corresponding serviceLocation
* @param {string} serviceLocation
* @param {number} throughput
* @private
*/
function _storeThroughputForServiceLocation(serviceLocation, throughput) {
if (!throughputList[serviceLocation]) {
throughputList[serviceLocation] = [];
}
throughputList[serviceLocation].push(throughput)
if (throughputList[serviceLocation].length > THROUGHPUT_SAMPLES) {
throughputList[serviceLocation].shift();
}
}
/**
* Adds a new service location entry to our list
* @param {object} e
* @param {string} type
* @private
*/
function _addToServiceLocationList(e, type) {
if (e && e.request && e.request.serviceLocation) {
const serviceLocation = e.request.serviceLocation;
if (serviceLocationList[type].all.indexOf(serviceLocation) === -1) {
serviceLocationList[type].all.push(serviceLocation)
}
serviceLocationList[type].current = serviceLocation;
}
}
/**
* Query DashAdapter and Service Description Controller to get the steering information defined in the manifest
* @returns {object}
*/
function getSteeringDataFromManifest() {
const manifest = manifestModel.getValue()
let contentSteeringData = adapter.getContentSteering(manifest);
if (!contentSteeringData) {
contentSteeringData = serviceDescriptionController.getServiceDescriptionSettings().contentSteering;
}
return contentSteeringData;
}
/**
* Should query steering server prior to playback start
* @returns {boolean}
*/
function shouldQueryBeforeStart() {
const steeringDataFromManifest = getSteeringDataFromManifest();
return !!steeringDataFromManifest && steeringDataFromManifest.queryBeforeStart;
}
/**
* Load the steering data from the steering server
* @returns {Promise}
*/
function loadSteeringData() {
return new Promise((resolve) => {
try {
const steeringDataFromManifest = getSteeringDataFromManifest();
if (!steeringDataFromManifest || !steeringDataFromManifest.serverUrl) {
resolve();
return;
}
const url = _getSteeringServerUrl(steeringDataFromManifest);
const request = new ContentSteeringRequest(url);
urlLoader.load({
request: request,
success: (data) => {
_handleSteeringResponse(data);
eventBus.trigger(MediaPlayerEvents.CONTENT_STEERING_REQUEST_COMPLETED, {
currentSteeringResponseData,
url
});
resolve();
},
error: (e, error, statusText, response) => {
_handleSteeringResponseError(e, response);
resolve(e);
},
complete: () => {
// Clear everything except for the current entry
serviceLocationList.baseUrl.all = _getClearedServiceLocationListAfterSteeringRequest(serviceLocationList.baseUrl);
serviceLocationList.location.all = _getClearedServiceLocationListAfterSteeringRequest(serviceLocationList.location);
}
});
} catch (e) {
resolve(e);
}
})
}
/**
* Return the cleared data of our serviceLocationList after the steering request was completed
* @param {object} data
* @returns {Object[]}
* @private
*/
function _getClearedServiceLocationListAfterSteeringRequest(data) {
if (!data.all || data.all.length === 0 || !data.current) {
return [];
}
return data.all.filter((entry) => {
return entry === data.current;
})
}
/**
* Returns the adjusted steering server url enhanced by pathway and throughput parameter
* @param {object} steeringDataFromManifest
* @returns {string}
* @private
*/
function _getSteeringServerUrl(steeringDataFromManifest) {
let url = steeringDataFromManifest.serverUrl;
if (currentSteeringResponseData && currentSteeringResponseData.reloadUri) {
if (urlUtils.isRelative(currentSteeringResponseData.reloadUri)) {
url = urlUtils.resolve(currentSteeringResponseData.reloadUri, steeringDataFromManifest.serverUrl);
} else {
url = currentSteeringResponseData.reloadUri;
}
}
const additionalQueryParameter = [];
const serviceLocations = serviceLocationList.baseUrl.all.concat(serviceLocationList.location.all);
if (serviceLocations.length > 0) {
// Derive throughput for each service Location
const data = serviceLocations.map((serviceLocation) => {
const throughput = _calculateThroughputForServiceLocation(serviceLocation);
return {
serviceLocation,
throughput
}
})
// Sort in descending order to put all elements without throughput (-1) in the end
data.sort((a, b) => {
return b.throughput - a.throughput
})
let pathwayString = '';
let throughputString = '';
data.forEach((entry, index) => {
if (index !== 0) {
pathwayString = `${pathwayString},`;
if (entry.throughput > -1) {
throughputString = `${throughputString},`;
}
}
pathwayString = `${pathwayString}${entry.serviceLocation}`;
if (entry.throughput > -1) {
throughputString = `${throughputString}${entry.throughput}`;
}
})
additionalQueryParameter.push({
key: QUERY_PARAMETER_KEYS.PATHWAY,
value: `"${pathwayString}"`
});
additionalQueryParameter.push({
key: QUERY_PARAMETER_KEYS.THROUGHPUT,
value: throughputString
});
}
url = Utils.addAditionalQueryParameterToUrl(url, additionalQueryParameter);
return url;
}
/**
* Calculate the arithmetic mean of the last throughput samples
* @param {string} serviceLocation
* @returns {number}
* @private
*/
function _calculateThroughputForServiceLocation(serviceLocation) {
if (!serviceLocation || !throughputList[serviceLocation] || throughputList[serviceLocation].length === 0) {
return -1;
}
const throughput = throughputList[serviceLocation].reduce((acc, curr) => {
return acc + curr;
}) / throughputList[serviceLocation].length;
return parseInt(throughput);
}
/**
* Parse the steering response and create instance of model ContentSteeringResponse
* @param {object} data
* @private
*/
function _handleSteeringResponse(data) {
if (!data || !data[DashConstants.CONTENT_STEERING_RESPONSE.VERSION] || parseInt(data[DashConstants.CONTENT_STEERING_RESPONSE.VERSION]) !== 1) {
return;
}
// Update the data for other classes to use
currentSteeringResponseData = new ContentSteeringResponse();
currentSteeringResponseData.version = data[DashConstants.CONTENT_STEERING_RESPONSE.VERSION];
if (data[DashConstants.CONTENT_STEERING_RESPONSE.TTL] && !isNaN(data[DashConstants.CONTENT_STEERING_RESPONSE.TTL])) {
currentSteeringResponseData.ttl = data[DashConstants.CONTENT_STEERING_RESPONSE.TTL];
}
if (data[DashConstants.CONTENT_STEERING_RESPONSE.RELOAD_URI]) {
currentSteeringResponseData.reloadUri = data[DashConstants.CONTENT_STEERING_RESPONSE.RELOAD_URI]
}
if (data[DashConstants.CONTENT_STEERING_RESPONSE.PATHWAY_PRIORITY]) {
currentSteeringResponseData.pathwayPriority = data[DashConstants.CONTENT_STEERING_RESPONSE.PATHWAY_PRIORITY]
}
if (data[DashConstants.CONTENT_STEERING_RESPONSE.PATHWAY_CLONES]) {
currentSteeringResponseData.pathwayClones = data[DashConstants.CONTENT_STEERING_RESPONSE.PATHWAY_CLONES]
currentSteeringResponseData.pathwayClones = currentSteeringResponseData.pathwayClones.filter((pathwayClone) => {
return _isValidPathwayClone(pathwayClone);
})
}
_startSteeringRequestTimer();
}
/**
* Checks if object is a valid PathwayClone
* @param {object} pathwayClone
* @returns {boolean}
* @private
*/
function _isValidPathwayClone(pathwayClone) {
return pathwayClone[DashConstants.CONTENT_STEERING_RESPONSE.BASE_ID]
&& pathwayClone[DashConstants.CONTENT_STEERING_RESPONSE.ID]
&& pathwayClone[DashConstants.CONTENT_STEERING_RESPONSE.URI_REPLACEMENT]
&& pathwayClone[DashConstants.CONTENT_STEERING_RESPONSE.URI_REPLACEMENT][DashConstants.CONTENT_STEERING_RESPONSE.HOST]
}
/**
* Returns synthesized BaseURL elements based on Pathway Cloning
* @param {BaseURL[]}referenceElements
* @returns {BaseURL[]}
*/
function getSynthesizedBaseUrlElements(referenceElements) {
try {
const synthesizedElements = _getSynthesizedElements(referenceElements);
return synthesizedElements.map((element) => {
const synthesizedBaseUrl = new BaseURL(element.synthesizedUrl, element.serviceLocation)
synthesizedBaseUrl.queryParams = element.queryParams;
synthesizedBaseUrl.dvbPriority = element.reference.dvbPriority;
synthesizedBaseUrl.dvbWeight = element.reference.dvbWeight;
synthesizedBaseUrl.availabilityTimeOffset = element.reference.availabilityTimeOffset;
synthesizedBaseUrl.availabilityTimeComplete = element.reference.availabilityTimeComplete;
return synthesizedBaseUrl;
})
} catch (e) {
logger.error(e);
return [];
}
}
/**
* Returns synthesized Location elements based on Pathway Cloning
* @param {MpdLocation[]} referenceElements
* @returns {MpdLocation[]}
*/
function getSynthesizedLocationElements(referenceElements) {
try {
const synthesizedElements = _getSynthesizedElements(referenceElements);
return synthesizedElements.map((element) => {
const synthesizedLocation = new MpdLocation(element.synthesizedUrl, element.serviceLocation)
synthesizedLocation.queryParams = element.queryParams;
return synthesizedLocation;
})
} catch (e) {
logger.error(e);
return [];
}
}
/**
* Helper function to synthesize elements
* @param {array} referenceElements
* @returns {array}
* @private
*/
function _getSynthesizedElements(referenceElements) {
try {
const synthesizedElements = [];
if (!referenceElements || referenceElements.length === 0 || !currentSteeringResponseData || !currentSteeringResponseData.pathwayClones || currentSteeringResponseData.pathwayClones.length === 0) {
return synthesizedElements;
}
currentSteeringResponseData.pathwayClones.forEach((pathwayClone) => {
let baseElements = referenceElements.filter((source) => {
return pathwayClone[DashConstants.CONTENT_STEERING_RESPONSE.BASE_ID] === source.serviceLocation;
})
let reference = null;
if (baseElements && baseElements.length > 0) {
reference = baseElements[0];
}
if (reference) {
const referenceUrl = new URL(reference.url);
let host = pathwayClone[DashConstants.CONTENT_STEERING_RESPONSE.URI_REPLACEMENT][DashConstants.CONTENT_STEERING_RESPONSE.HOST];
host = Utils.stringHasProtocol(host) ? host : `${referenceUrl.protocol}//${host}`;
const synthesizedElement =
{
synthesizedUrl: `${host}${referenceUrl.pathname}`,
serviceLocation: pathwayClone[DashConstants.CONTENT_STEERING_RESPONSE.ID],
queryParams: pathwayClone[DashConstants.CONTENT_STEERING_RESPONSE.URI_REPLACEMENT][DashConstants.CONTENT_STEERING_RESPONSE.PARAMS],
reference
};
synthesizedElements.push(synthesizedElement);
}
});
return synthesizedElements;
} catch (e) {
logger.error(e);
return [];
}
}
/**
* Start timeout for next steering request
* @private
*/
function _startSteeringRequestTimer() {
// Start timer for next request
if (currentSteeringResponseData && currentSteeringResponseData.ttl && !isNaN(currentSteeringResponseData.ttl)) {
if (nextRequestTimer) {
clearTimeout(nextRequestTimer);
}
nextRequestTimer = setTimeout(() => {
loadSteeringData();
}, currentSteeringResponseData.ttl * 1000);
}
}
/**
* Stop timeout for next steering request
*/
function stopSteeringRequestTimer() {
if (nextRequestTimer) {
clearTimeout(nextRequestTimer);
}
nextRequestTimer = null;
}
/**
* Handle errors that occured when querying the steering server
* @param {object} e
* @param {object} response
* @private
*/
function _handleSteeringResponseError(e, response) {
try {
logger.warn(`Error fetching data from content steering server`, e);
const statusCode = response.status;
switch (statusCode) {
// 410 response code. Stop steering
case 410:
break;
// 429 Too Many Requests. Replace existing TTL value with Retry-After header if present
case 429:
const retryAfter = response && response.getResponseHeader ? response.getResponseHeader('retry-after') : null;
if (retryAfter !== null) {
if (!currentSteeringResponseData) {
currentSteeringResponseData = {};
}
currentSteeringResponseData.ttl = parseInt(retryAfter);
}
_startSteeringRequestTimer();
break;
default:
_startSteeringRequestTimer();
break;
}
} catch (e) {
logger.error(e);
}
}
/**
* Returns the currentSteeringResponseData
* @returns {ContentSteeringResponse}
*/
function getCurrentSteeringResponseData() {
return currentSteeringResponseData;
}
function reset() {
_resetInitialSettings();
eventBus.off(MediaPlayerEvents.FRAGMENT_LOADING_STARTED, _onFragmentLoadingStarted, instance);
eventBus.off(MediaPlayerEvents.MANIFEST_LOADING_STARTED, _onManifestLoadingStarted, instance);
eventBus.off(MediaPlayerEvents.MANIFEST_LOADING_FINISHED, _onManifestLoadingFinished, instance);
eventBus.off(MediaPlayerEvents.THROUGHPUT_MEASUREMENT_STORED, _onThroughputMeasurementStored, instance);
}
function _resetInitialSettings() {
currentSteeringResponseData = null;
throughputList = {};
serviceLocationList = {
baseUrl: {
current: null,
all: []
},
location: {
current: null,
all: []
}
};
stopSteeringRequestTimer()
}
instance = {
reset,
setConfig,
loadSteeringData,
getCurrentSteeringResponseData,
shouldQueryBeforeStart,
getSteeringDataFromManifest,
stopSteeringRequestTimer,
getSynthesizedBaseUrlElements,
getSynthesizedLocationElements,
initialize
};
setup();
return instance;
}
ContentSteeringController.__dashjs_factory_name = 'ContentSteeringController';
export default FactoryMaker.getSingletonFactory(ContentSteeringController);