Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add license url parsing #1644

Merged
merged 16 commits into from
Dec 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 204 additions & 2 deletions lib/dash/content_protection.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,188 @@ shaka.dash.ContentProtection.parseFromRepresentation = function(
};


/**
* Gets a Widevine license URL from a content protection element
* containing a custom `ms:laurl` element
*
* @param {shaka.dash.ContentProtection.Element} element
* @return {string}
*/
shaka.dash.ContentProtection.getWidevineLicenseUrl = function(element) {
const mslaurlNode = shaka.util.XmlUtils.findChildNS(
element.node, 'urn:microsoft', 'laurl');
if (mslaurlNode) {
return mslaurlNode.getAttribute('licenseUrl') || '';
}
return '';
};


/**
* @typedef {{
* type: number,
* value: !Uint8Array
* }}
*
* @description
* The parsed result of a PlayReady object record.
*
* @property {number} type
* Type of data stored in the record.
* @property {!Uint8Array} value
* Record content.
*/
shaka.dash.ContentProtection.PlayReadyRecord;

/**
* Enum for PlayReady record types.
* @enum {number}
*/
shaka.dash.ContentProtection.PLAYREADY_RECORD_TYPES = {
RIGHTS_MANAGEMENT: 0x001,
RESERVED: 0x002,
EMBEDDED_LICENSE: 0x003,
};


/**
* Parses an Array buffer starting at byteOffset for
* PlayReady Object Records. Each PRO Record is
* preceeded by it's PlayReady Record type and length
* in bytes.
*
* PlayReady Object Record format: https://goo.gl/FTcu46
*
* @param {!ArrayBuffer} recordData
* @param {number} byteOffset
* @return {!Array.<shaka.dash.ContentProtection.PlayReadyRecord>}
* @private
*/
shaka.dash.ContentProtection.parseMsProRecords_ = function(
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
recordData, byteOffset) {
const records = [];
const view = new DataView(recordData);

while (byteOffset < recordData.byteLength - 1) {
const type = view.getUint16(byteOffset, true);
byteOffset += 2;

const byteLength = view.getUint16(byteOffset, true);
byteOffset += 2;

goog.asserts.assert(
(byteLength & 1) === 0,
'expected byteLength to be an even number');

const recordValue = new Uint8Array(recordData, byteOffset, byteLength);

records.push({
type: type,
value: recordValue,
});

byteOffset += byteLength;
}

return records;
};


/**
* Parses an ArrayBuffer for PlayReady Objects. The data
* should contain a 32-bit integer indicating the length of
* the PRO in bytes. Following that, a 16-bit integer for
* the number of PlayReady Object Records in the PRO. Lastly,
* a byte array of the PRO Records themselves.
*
* PlayReady Object format: https://goo.gl/W8yAN4
*
* @param {!ArrayBuffer} data
* @return {!Array.<shaka.dash.ContentProtection.PlayReadyRecord>}
* @private
*/
shaka.dash.ContentProtection.parseMsPro_ = function(data) {
let byteOffset = 0;
const view = new DataView(data);

// First 4 bytes is the PRO length (DWORD)
const byteLength = view.getUint32(byteOffset, true /* littleEndian */);
byteOffset += 4;

if (byteLength !== data.byteLength) {
// Malformed PRO
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
shaka.log.warning('PlayReady Object with invalid length encountered.');
return [];
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
}

// Skip PRO Record count (WORD)
byteOffset += 2;

// Rest of the data contains the PRO Records
const ContentProtection = shaka.dash.ContentProtection;
return ContentProtection.parseMsProRecords_(data, byteOffset);
};


/**
* PlayReady Header format: https://goo.gl/dBzxNA
*
* @param {!Element} xml
* @return {string}
* @private
*/
shaka.dash.ContentProtection.getLaurl_ = function(xml) {
// LA_URL element is optional and no more than one is
// allowed inside the DATA element. Only absolute URLs are allowed.
// If the LA_URL element exists, it must not be empty.
const laurlNode = xml.querySelector('DATA > LA_URL');
if (laurlNode) {
return laurlNode.textContent;
}

// Not found
return '';
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
};


/**
* Gets a PlayReady license URL from a content protection element
* containing a PlayReady Header Object
*
* @param {shaka.dash.ContentProtection.Element} element
* @return {string}
*/
shaka.dash.ContentProtection.getPlayReadyLicenseUrl = function(element) {
const proNode = shaka.util.XmlUtils.findChildNS(
element.node, 'urn:microsoft:playready', 'pro');

if (!proNode) {
return '';
}

const ContentProtection = shaka.dash.ContentProtection;
const PLAYREADY_RECORD_TYPES = ContentProtection.PLAYREADY_RECORD_TYPES;

const bytes = shaka.util.Uint8ArrayUtils.fromBase64(proNode.textContent);
const records = ContentProtection.parseMsPro_(bytes.buffer);
const record = records.filter((record) => {
return record.type === PLAYREADY_RECORD_TYPES.RIGHTS_MANAGEMENT;
})[0];

if (!record) {
return '';
}

const xml = shaka.util.StringUtils.fromUTF16(record.value, true);
const rootElement = shaka.util.XmlUtils.parseXmlString(xml, 'WRMHEADER');
if (!rootElement) {
return '';
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
}

return ContentProtection.getLaurl_(rootElement);
};


/**
* Creates DrmInfo objects from the given element.
*
Expand All @@ -272,9 +454,11 @@ shaka.dash.ContentProtection.parseFromRepresentation = function(
* @private
*/
shaka.dash.ContentProtection.convertElements_ = function(
defaultInit, callback, elements) {
defaultInit, callback, elements) {
const ContentProtection = shaka.dash.ContentProtection;
const createDrmInfo = shaka.util.ManifestParserUtils.createDrmInfo;
const defaultKeySystems = shaka.dash.ContentProtection.defaultKeySystems_;
const defaultKeySystems = ContentProtection.defaultKeySystems_;
const licenseUrlParsers = ContentProtection.licenseUrlParsers_;

/** @type {!Array.<shaka.extern.DrmInfo>} */
const out = [];
Expand All @@ -288,6 +472,11 @@ shaka.dash.ContentProtection.convertElements_ = function(

const initData = element.init || defaultInit;
const info = createDrmInfo(keySystem, initData);
const licenseParser = licenseUrlParsers.get(keySystem);
if (licenseParser) {
info.licenseServerUri = licenseParser(element);
}

out.push(info);
} else {
goog.asserts.assert(callback, 'ContentProtection callback is required');
Expand All @@ -302,6 +491,19 @@ shaka.dash.ContentProtection.convertElements_ = function(
};


/**
* A map of key system name to license server url parser.
*
* @const {!Map.<string, function(shaka.dash.ContentProtection.Element)>}
* @private
*/
shaka.dash.ContentProtection.licenseUrlParsers_ = new Map()
.set('com.widevine.alpha',
shaka.dash.ContentProtection.getWidevineLicenseUrl)
.set('com.microsoft.playready',
shaka.dash.ContentProtection.getPlayReadyLicenseUrl);


/**
* Parses the given ContentProtection elements. If there is an error, it
* removes those elements.
Expand Down
2 changes: 1 addition & 1 deletion lib/dash/dash_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ shaka.dash.DashParser.prototype.parseManifest_ =
const Error = shaka.util.Error;
const MpdUtils = shaka.dash.MpdUtils;

let mpd = MpdUtils.parseXml(data, 'MPD');
let mpd = shaka.util.XmlUtils.parseXml(data, 'MPD');
if (!mpd) {
throw new Error(
Error.Severity.CRITICAL, Error.Category.MANIFEST,
Expand Down
33 changes: 1 addition & 32 deletions lib/dash/mpd_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ goog.require('shaka.util.AbortableOperation');
goog.require('shaka.util.Error');
goog.require('shaka.util.Functional');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.XmlUtils');


Expand Down Expand Up @@ -433,36 +432,6 @@ shaka.dash.MpdUtils.inheritChild = function(context, callback, child) {
};


/**
* 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) {
let parser = new DOMParser();
let rootElem;
let xml;
try {
let 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.
* It also strips the xlink properties off of the element,
Expand Down Expand Up @@ -536,7 +505,7 @@ shaka.dash.MpdUtils.handleXlinkInElement_ =
return networkOperation.chain((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.
let rootElem = MpdUtils.parseXml(response.data, element.tagName);
let rootElem = shaka.util.XmlUtils.parseXml(response.data, element.tagName);
if (!rootElem) {
// It was not valid XML.
return shaka.util.AbortableOperation.failed(new Error(
Expand Down
63 changes: 63 additions & 0 deletions lib/util/xml_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
goog.provide('shaka.util.XmlUtils');

goog.require('shaka.log');
goog.require('shaka.util.StringUtils');


/**
Expand All @@ -43,6 +44,24 @@ shaka.util.XmlUtils.findChild = function(elem, name) {
};


/**
* Finds a namespace-qualified child XML element.
* @param {!Node} elem The parent XML element.
* @param {string} ns The child XML element's namespace URI.
* @param {string} name The child XML element's local name.
* @return {Element} The child XML element, or null if a child XML element does
* not exist with the given tag name OR if there exists more than one
* child XML element with the given tag name.
*/
shaka.util.XmlUtils.findChildNS = function(elem, ns, name) {
let children = shaka.util.XmlUtils.findChildrenNS(elem, ns, name);
if (children.length != 1) {
return null;
}
return children[0];
};


/**
* Finds child XML elements.
* @param {!Node} elem The parent XML element.
Expand Down Expand Up @@ -286,3 +305,47 @@ shaka.util.XmlUtils.evalDivision = function(exprString) {
}
return !isNaN(n) ? n : null;
};


/**
* Parse a string and return the resulting root element if
* it was valid XML.
* @param {string} xmlString
* @param {string} expectedRootElemName
* @return {Element|undefined}
*/
shaka.util.XmlUtils.parseXmlString = function(xmlString, expectedRootElemName) {
const parser = new DOMParser();
let rootElem;
let xml;
try {
xml = parser.parseFromString(xmlString, '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;
};


/**
* 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.util.XmlUtils.parseXml = function(data, expectedRootElemName) {
try {
const string = shaka.util.StringUtils.fromUTF8(data);
return shaka.util.XmlUtils.parseXmlString(string, expectedRootElemName);
} catch (exception) {}
};
Loading