/**
* 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 XlinkLoader from '../XlinkLoader';
import EventBus from '../../core/EventBus';
import Events from '../../core/events/Events';
import FactoryMaker from '../../core/FactoryMaker';
import X2JS from '../../../externals/xml2json';
import URLUtils from '../utils/URLUtils';
import DashConstants from '../../dash/constants/DashConstants';
const RESOLVE_TYPE_ONLOAD = 'onLoad';
const RESOLVE_TYPE_ONACTUATE = 'onActuate';
const RESOLVE_TO_ZERO = 'urn:mpeg:dash:resolve-to-zero:2013';
function XlinkController(config) {
config = config || {};
let context = this.context;
let eventBus = EventBus(context).getInstance();
const urlUtils = URLUtils(context).getInstance();
let instance,
matchers,
iron,
manifest,
converter,
xlinkLoader;
function setup() {
eventBus.on(Events.XLINK_ELEMENT_LOADED, onXlinkElementLoaded, instance);
xlinkLoader = XlinkLoader(context).create({
errHandler: config.errHandler,
dashMetrics: config.dashMetrics,
mediaPlayerModel: config.mediaPlayerModel,
requestModifier: config.requestModifier,
settings: config.settings
});
}
function setMatchers(value) {
if (value) {
matchers = value;
}
}
function setIron(value) {
if (value) {
iron = value;
}
}
/**
* <p>Triggers the resolution of the xlink.onLoad attributes in the manifest file </p>
* @param {Object} mpd - the manifest
*/
function resolveManifestOnLoad(mpd) {
let elements;
// First resolve all periods, so unnecessary requests inside onLoad Periods with Default content are avoided
converter = new X2JS({
escapeMode: false,
attributePrefix: '',
arrayAccessForm: 'property',
emptyNodeForm: 'object',
stripWhitespaces: false,
enableToStringFunc: false,
ignoreRoot: true,
matchers: matchers
});
manifest = mpd;
if (manifest.Period_asArray) {
elements = getElementsToResolve(manifest.Period_asArray, manifest, DashConstants.PERIOD, RESOLVE_TYPE_ONLOAD);
resolve(elements, DashConstants.PERIOD, RESOLVE_TYPE_ONLOAD);
} else {
eventBus.trigger(Events.XLINK_READY, {manifest: manifest});
}
}
function reset() {
eventBus.off(Events.XLINK_ELEMENT_LOADED, onXlinkElementLoaded, instance);
if (xlinkLoader) {
xlinkLoader.reset();
xlinkLoader = null;
}
}
function resolve(elements, type, resolveType) {
let resolveObject = {};
let element,
url;
resolveObject.elements = elements;
resolveObject.type = type;
resolveObject.resolveType = resolveType;
// If nothing to resolve, directly call allElementsLoaded
if (resolveObject.elements.length === 0) {
onXlinkAllElementsLoaded(resolveObject);
}
for (let i = 0; i < resolveObject.elements.length; i++) {
element = resolveObject.elements[i];
if (urlUtils.isHTTPURL(element.url)) {
url = element.url;
} else {
url = element.originalContent.BaseURL + element.url;
}
xlinkLoader.load(url, element, resolveObject);
}
}
function onXlinkElementLoaded(event) {
let element,
resolveObject;
const openingTag = '<response>';
const closingTag = '</response>';
let mergedContent = '';
element = event.element;
resolveObject = event.resolveObject;
// if the element resolved into content parse the content
if (element.resolvedContent) {
let index = 0;
// we add a parent elements so the converter is able to parse multiple elements of the same type which are not wrapped inside a container
if (element.resolvedContent.indexOf('<?xml') === 0) {
index = element.resolvedContent.indexOf('?>') + 2; //find the closing position of the xml declaration, if it exists.
}
mergedContent = element.resolvedContent.substr(0,index) + openingTag + element.resolvedContent.substr(index) + closingTag;
element.resolvedContent = converter.xml_str2json(mergedContent);
}
if (isResolvingFinished(resolveObject)) {
onXlinkAllElementsLoaded(resolveObject);
}
}
// We got to wait till all elements of the current queue are resolved before merging back
function onXlinkAllElementsLoaded (resolveObject) {
let elements = [];
let i,
obj;
mergeElementsBack(resolveObject);
if (resolveObject.resolveType === RESOLVE_TYPE_ONACTUATE) {
eventBus.trigger(Events.XLINK_READY, { manifest: manifest });
}
if (resolveObject.resolveType === RESOLVE_TYPE_ONLOAD) {
switch (resolveObject.type) {
// Start resolving the other elements. We can do Adaptation Set and EventStream in parallel
case DashConstants.PERIOD:
for (i = 0; i < manifest[DashConstants.PERIOD + '_asArray'].length; i++) {
obj = manifest[DashConstants.PERIOD + '_asArray'][i];
if (obj.hasOwnProperty(DashConstants.ADAPTATION_SET + '_asArray')) {
elements = elements.concat(getElementsToResolve(obj[DashConstants.ADAPTATION_SET + '_asArray'], obj, DashConstants.ADAPTATION_SET, RESOLVE_TYPE_ONLOAD));
}
if (obj.hasOwnProperty(DashConstants.EVENT_STREAM + '_asArray')) {
elements = elements.concat(getElementsToResolve(obj[DashConstants.EVENT_STREAM + '_asArray'], obj, DashConstants.EVENT_STREAM, RESOLVE_TYPE_ONLOAD));
}
}
resolve(elements, DashConstants.ADAPTATION_SET, RESOLVE_TYPE_ONLOAD);
break;
case DashConstants.ADAPTATION_SET:
// TODO: Resolve SegmentList here
eventBus.trigger(Events.XLINK_READY, { manifest: manifest });
break;
}
}
}
// Returns the elements with the specific resolve Type
function getElementsToResolve(elements, parentElement, type, resolveType) {
let toResolve = [];
let element,
i,
xlinkObject;
// first remove all the resolve-to-zero elements
for (i = elements.length - 1; i >= 0; i--) {
element = elements[i];
if (element.hasOwnProperty('xlink:href') && element['xlink:href'] === RESOLVE_TO_ZERO) {
elements.splice(i, 1);
}
}
// now get the elements with the right resolve type
for (i = 0; i < elements.length; i++) {
element = elements[i];
if (element.hasOwnProperty('xlink:href') && element.hasOwnProperty('xlink:actuate') && element['xlink:actuate'] === resolveType) {
xlinkObject = createXlinkObject(element['xlink:href'], parentElement, type, i, resolveType, element);
toResolve.push(xlinkObject);
}
}
return toResolve;
}
function mergeElementsBack(resolveObject) {
let resolvedElements = [];
let element,
type,
obj,
i,
j,
k;
// Start merging back from the end because of index shifting. Note that the elements with the same parent have to be ordered by index ascending
for (i = resolveObject.elements.length - 1; i >= 0; i --) {
element = resolveObject.elements[i];
type = element.type + '_asArray';
// Element couldn't be resolved or is TODO Inappropriate target: Remove all Xlink attributes
if (!element.resolvedContent || isInappropriateTarget()) {
delete element.originalContent['xlink:actuate'];
delete element.originalContent['xlink:href'];
resolvedElements.push(element.originalContent);
}
// Element was successfully resolved
else if (element.resolvedContent) {
for (j = 0; j < element.resolvedContent[type].length; j++) {
//TODO Contains another Xlink attribute with xlink:actuate set to onload. Remove all xLink attributes
obj = element.resolvedContent[type][j];
resolvedElements.push(obj);
}
}
// Replace the old elements in the parent with the resolved ones
element.parentElement[type].splice(element.index, 1);
for (k = 0; k < resolvedElements.length; k++) {
element.parentElement[type].splice(element.index + k, 0, resolvedElements[k]);
}
resolvedElements = [];
}
if (resolveObject.elements.length > 0) {
iron.run(manifest);
}
}
function createXlinkObject(url, parentElement, type, index, resolveType, originalContent) {
return {
url: url,
parentElement: parentElement,
type: type,
index: index,
resolveType: resolveType,
originalContent: originalContent,
resolvedContent: null,
resolved: false
};
}
// Check if all pending requests are finished
function isResolvingFinished(elementsToResolve) {
let i,
obj;
for (i = 0; i < elementsToResolve.elements.length; i++) {
obj = elementsToResolve.elements[i];
if (obj.resolved === false) {
return false;
}
}
return true;
}
// TODO : Do some syntax check here if the target is valid or not
function isInappropriateTarget() {
return false;
}
instance = {
resolveManifestOnLoad: resolveManifestOnLoad,
setMatchers: setMatchers,
setIron: setIron,
reset: reset
};
setup();
return instance;
}
XlinkController.__dashjs_factory_name = 'XlinkController';
export default FactoryMaker.getClassFactory(XlinkController);