Skip to content

Commit

Permalink
Added basic xlink support.
Browse files Browse the repository at this point in the history
This adds a utility to mpd_utils.js that filters a manifest and
automatically downloads and substitutes in the contents of any xlink
link.

This only supports xlink:actuate="onLoad"; "onRequest" would require
significant changes to our manifest processing pipeline.
It also adds a new field to INVALID_XML errors to indicate which
xml was invalid, to make the error more informative when called on
xml loaded by xlink links.

Also added a demo asset.
This is just a simple modification of heliocentrism to break it into
multiple files.

Closes #587

Change-Id: If87b1e78e65261dcc4e043b0c2e6cf69c1b12e08
  • Loading branch information
theodab committed May 8, 2017
1 parent 40aaa3d commit 723e6ad
Show file tree
Hide file tree
Showing 8 changed files with 585 additions and 25 deletions.
16 changes: 16 additions & 0 deletions demo/assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ shakaAssets.Feature = {
MULTIKEY: 'multiple keys',
MULTIPERIOD: 'multiple Periods',
TRICK_MODE: 'special trick mode track',
XLINK: 'xlink',

SUBTITLES: 'subtitles',
CAPTIONS: 'captions',
Expand Down Expand Up @@ -481,6 +482,21 @@ shakaAssets.testAssets = [
shakaAssets.Feature.WEBM
]
},
{
name: 'Heliocentrism (multicodec, multiperiod, xlink)',
manifestUri: '//storage.googleapis.com/shaka-demo-assets/heliocentrism-xlink/heliocentrism.mpd', // gjslint: disable=110

encoder: shakaAssets.Encoder.SHAKA_PACKAGER,
source: shakaAssets.Source.SHAKA,
drm: [],
features: [
shakaAssets.Feature.MP4,
shakaAssets.Feature.MULTIPERIOD,
shakaAssets.Feature.SEGMENT_BASE,
shakaAssets.Feature.WEBM,
shakaAssets.Feature.XLINK
]
},
{
name: '"Dig the Uke" by Stefan Kartenberg (audio only, multicodec)', // gjslint: disable=110
// From: http://dig.ccmixter.org/files/JeffSpeed68/53327
Expand Down
51 changes: 29 additions & 22 deletions lib/dash/dash_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -410,34 +410,41 @@ shaka.dash.DashParser.prototype.requestManifest_ = function() {
shaka.dash.DashParser.prototype.parseManifest_ =
function(data, finalManifestUri) {
var Error = shaka.util.Error;
var Functional = shaka.util.Functional;
var XmlUtils = shaka.util.XmlUtils;
var ManifestParserUtils = shaka.util.ManifestParserUtils;
var MpdUtils = shaka.dash.MpdUtils;

var string = shaka.util.StringUtils.fromUTF8(data);
var parser = new DOMParser();
var xml = null;
var mpd = null;

try {
xml = parser.parseFromString(string, 'text/xml');
} catch (exception) {}
if (xml) {
// parseFromString returns a Document object. A Document is a Node but not
// an Element, so it cannot be used in XmlUtils (technically it can but the
// types don't match). The |documentElement| member defines the top-level
// element in the document.
if (xml.documentElement.tagName == 'MPD')
mpd = xml.documentElement;
}
if (mpd && mpd.getElementsByTagName('parsererror').length > 0)
mpd = null; // It had a parser error in it.
var mpd = MpdUtils.parseXml(data, 'MPD');
if (!mpd) {
throw new Error(
Error.Severity.CRITICAL, Error.Category.MANIFEST,
Error.Code.DASH_INVALID_XML);
Error.Code.DASH_INVALID_XML, finalManifestUri);
}

// Process the mpd to account for xlink connections.
var xlinkPromise = MpdUtils.processXlinks(
mpd, this.config_.retryParameters, finalManifestUri,
this.playerInterface_.networkingEngine);
return xlinkPromise.then(function(finalMpd) {
return this.processManifest_(finalMpd, finalManifestUri);
}.bind(this));
};


/**
* Taked a formatted MPD and converts it into a manifest.
*
* @param {!Element} mpd
* @param {string} finalManifestUri The final manifest URI, which may
* differ from this.manifestUri_ if there has been a redirect.
* @return {!Promise}
* @throws shaka.util.Error When there is a parsing error.
* @private
*/
shaka.dash.DashParser.prototype.processManifest_ =
function(mpd, finalManifestUri) {
var Functional = shaka.util.Functional;
var XmlUtils = shaka.util.XmlUtils;
var ManifestParserUtils = shaka.util.ManifestParserUtils;

// Get any Location elements. This will update the manifest location and
// the base URI.
/** @type {!Array.<string>} */
Expand Down
164 changes: 164 additions & 0 deletions lib/dash/mpd_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ goog.provide('shaka.dash.MpdUtils');

goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.Functional');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.XmlUtils');


Expand Down Expand Up @@ -392,3 +394,165 @@ shaka.dash.MpdUtils.inheritChild = function(context, callback, child) {
.map(function(s) { return XmlUtils.findChild(s, child); })
.reduce(function(all, part) { return all || part; });
};


/**
* Parse some UTF8 data and return the resulting root element if
* it was valid XML.
* @param {ArrayBuffer} data
* @param {!string} expectedRootElemName
* @return {Element|undefined}
*/
shaka.dash.MpdUtils.parseXml = function(data, expectedRootElemName) {
var parser = new DOMParser();
var rootElem;
var xml;
try {
var string = shaka.util.StringUtils.fromUTF8(data);
xml = parser.parseFromString(string, 'text/xml');
} catch (exception) {}
if (xml) {
// The top-level element in the loaded xml should have the
// same type as the element linking.
if (xml.documentElement.tagName == expectedRootElemName)
rootElem = xml.documentElement;
}
if (rootElem && rootElem.getElementsByTagName('parsererror').length > 0)
return null; // It had a parser error in it.

return rootElem;
};


/**
* Follow the xlink link contained in the given element.
*
* @param {!Element} element
* @param {!shakaExtern.RetryParameters} retryParameters
* @param {!string} baseUri
* @param {!shaka.net.NetworkingEngine} networkingEngine
* @param {number} linkDepth
* @return {!Promise.<!Element>}
* @private
*/
shaka.dash.MpdUtils.handleXlinkInElement_ =
function(element, retryParameters, baseUri, networkingEngine, linkDepth) {
var MpdUtils = shaka.dash.MpdUtils;
var Error = shaka.util.Error;
var ManifestParserUtils = shaka.util.ManifestParserUtils;

if (linkDepth >= 5) {
return Promise.reject(new Error(
Error.Severity.CRITICAL, Error.Category.MANIFEST,
Error.Code.DASH_XLINK_DEPTH_LIMIT));
}

var xlinkHref = element.getAttribute('xlink:href');
var xlinkActuate = element.getAttribute('xlink:actuate') || 'onRequest';
if (xlinkActuate != 'onLoad') {
// Only xlink:actuate="onLoad" is supported.
// When no value is specified, the assumed value is "onRequest".
return Promise.reject(new Error(
Error.Severity.CRITICAL, Error.Category.MANIFEST,
Error.Code.DASH_UNSUPPORTED_XLINK_ACTUATE));
}

// Remove the xlink properties, so it won't download again
// when re-processed.
for (var i = 0; i < element.attributes.length; i++) {
var attribute = element.attributes[i].nodeName;
if (attribute.indexOf('xlink:') != -1) {
element.removeAttribute(attribute);
i -= 1;
}
}

// Resolve the xlink href, in case it's a relative URL.
var uris = ManifestParserUtils.resolveUris([baseUri], [xlinkHref]);

// Load in the linked elements.
var requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
var request = shaka.net.NetworkingEngine.makeRequest(
uris, retryParameters);
var requestPromise = networkingEngine.request(requestType, request);
return requestPromise.then(function(response) {
// This only supports the case where the loaded xml has a single
// top-level element. If there are multiple roots, it will be rejected.
var rootElem = MpdUtils.parseXml(response.data, element.tagName);
if (!rootElem) {
// It was not valid XML.
return Promise.reject(new Error(
Error.Severity.CRITICAL, Error.Category.MANIFEST,
Error.Code.DASH_INVALID_XML, xlinkHref));
}

// Remove the current contents of the node.
while (element.childNodes.length)
element.removeChild(element.childNodes[0]);

// Move the children of the loaded xml into the current element.
while (rootElem.childNodes.length) {
var child = rootElem.childNodes[0];
rootElem.removeChild(child);
element.appendChild(child);
}

// Move the attributes of the loaded xml into the current element.
for (var i = 0; i < rootElem.attributes.length; i++) {
var attribute = rootElem.attributes[i].nodeName;
var attributeValue = rootElem.getAttribute(attribute);
element.setAttribute(attribute, attributeValue);
}

return shaka.dash.MpdUtils.processXlinks(
element, retryParameters, xlinkHref, networkingEngine,
linkDepth + 1);
}.bind(element));
};


/**
* Filter the contents of a node recursively, replacing xlink links
* with their associated online data.
*
* @param {!Element} element
* @param {!shakaExtern.RetryParameters} retryParameters
* @param {!string} baseUri
* @param {!shaka.net.NetworkingEngine} networkingEngine
* @param {number=} opt_linkDepth
* @return {!Promise.<!Element>}
*/
shaka.dash.MpdUtils.processXlinks =
function(element, retryParameters, baseUri, networkingEngine,
opt_linkDepth) {
var MpdUtils = shaka.dash.MpdUtils;
opt_linkDepth = opt_linkDepth || 0;

if (element.getAttribute('xlink:href'))
return MpdUtils.handleXlinkInElement_(
element, retryParameters, baseUri, networkingEngine, opt_linkDepth);

// Filter out any children that should be nulled.
for (var i = 0; i < element.children.length; i++) {
var child = element.children[i];
var resolveToZeroString = 'urn:mpeg:dash:resolve-to-zero:2013';
if (child.getAttribute('xlink:href') == resolveToZeroString) {
// This is a 'resolve to zero' code; it means the element should
// be removed, as specified by the mpeg-dash rules for xlink.
element.removeChild(child);
i -= 1;
}
}

var childPromises = [];
for (var i = 0; i < element.children.length; i++) {
// Replace the child with its processed form.
var childPromise = shaka.dash.MpdUtils.processXlinks(
element.children[i], retryParameters, baseUri, networkingEngine,
opt_linkDepth);
childPromises.push(childPromise);
}
return Promise.all(childPromises).then(function() {
return element;
});
};
13 changes: 13 additions & 0 deletions lib/util/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ shaka.util.Error.Code = {
/**
* The XML parser failed to parse an xml stream, or the XML lacks mandatory
* elements for TTML.
* <br> error.data[0] is the URI associated with the XML.
*/
'INVALID_XML': 2005,

Expand Down Expand Up @@ -525,6 +526,18 @@ shaka.util.Error.Code = {
*/
'HLS_KEYFORMATS_NOT_SUPPORTED': 4026,

/**
* The manifest parser only supports xlink links with
* xlink:actuate="onLoad".
*/
'DASH_UNSUPPORTED_XLINK_ACTUATE': 4027,

/**
* The manifest parser has hit its depth limit on
* xlink link chains.
*/
'DASH_XLINK_DEPTH_LIMIT': 4028,


// RETIRED: 'INCONSISTENT_BUFFER_STATE': 5000,
// RETIRED: 'INVALID_SEGMENT_INDEX': 5001,
Expand Down
9 changes: 6 additions & 3 deletions test/dash/dash_parser_manifest_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,8 @@ describe('DashParser Manifest', function() {
var error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_INVALID_XML);
shaka.util.Error.Code.DASH_INVALID_XML,
'dummy://foo');
Dash.testFails(done, source, error);
});

Expand All @@ -645,7 +646,8 @@ describe('DashParser Manifest', function() {
var error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_INVALID_XML);
shaka.util.Error.Code.DASH_INVALID_XML,
'dummy://foo');
Dash.testFails(done, source, error);
});

Expand All @@ -667,7 +669,8 @@ describe('DashParser Manifest', function() {
var error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_INVALID_XML);
shaka.util.Error.Code.DASH_INVALID_XML,
'dummy://foo');
Dash.testFails(done, source, error);
});

Expand Down
Loading

0 comments on commit 723e6ad

Please sign in to comment.