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 6 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
228 changes: 195 additions & 33 deletions lib/dash/content_protection.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,22 +92,22 @@ shaka.dash.ContentProtection.Element;
* @private
*/
shaka.dash.ContentProtection.defaultKeySystems_ = new Map()
.set('urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b',
'org.w3.clearkey')
.set('urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
'com.widevine.alpha')
.set('urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95',
'com.microsoft.playready')
.set('urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb',
'com.adobe.primetime');
.set('urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b',
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
'org.w3.clearkey')
.set('urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
'com.widevine.alpha')
.set('urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95',
'com.microsoft.playready')
.set('urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb',
'com.adobe.primetime');


/**
* @const {string}
* @private
*/
shaka.dash.ContentProtection.MP4Protection_ =
'urn:mpeg:dash:mp4protection:2011';
'urn:mpeg:dash:mp4protection:2011';
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved


/**
Expand All @@ -126,7 +126,7 @@ shaka.dash.ContentProtection.CencNamespaceUri_ = 'urn:mpeg:cenc:2013';
* @return {shaka.dash.ContentProtection.Context}
*/
shaka.dash.ContentProtection.parseFromAdaptationSet = function(
elems, callback, ignoreDrmInfo) {
elems, callback, ignoreDrmInfo) {
vaage marked this conversation as resolved.
Show resolved Hide resolved
const ContentProtection = shaka.dash.ContentProtection;
const ManifestParserUtils = shaka.util.ManifestParserUtils;
let parsed = ContentProtection.parseElements_(elems);
Expand All @@ -143,9 +143,9 @@ shaka.dash.ContentProtection.parseFromAdaptationSet = function(

if (keyIds.size > 1) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_CONFLICTING_KEY_IDS);
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_CONFLICTING_KEY_IDS);
}

if (!ignoreDrmInfo) {
Expand All @@ -154,7 +154,7 @@ shaka.dash.ContentProtection.parseFromAdaptationSet = function(
parsedNonCenc = parsed.filter(function(elem) {
if (elem.schemeUri == ContentProtection.MP4Protection_) {
goog.asserts.assert(!elem.init || elem.init.length,
'Init data must be null or non-empty.');
'Init data must be null or non-empty.');
defaultInit = elem.init || defaultInit;
return false;
} else {
Expand All @@ -164,7 +164,7 @@ shaka.dash.ContentProtection.parseFromAdaptationSet = function(

if (parsedNonCenc.length) {
drmInfos = ContentProtection.convertElements_(
defaultInit, callback, parsedNonCenc);
defaultInit, callback, parsedNonCenc);

// If there are no drmInfos after parsing, then add a dummy entry.
// This may be removed in parseKeyIds.
Expand Down Expand Up @@ -222,14 +222,14 @@ shaka.dash.ContentProtection.parseFromAdaptationSet = function(
* @return {?string} The parsed key ID
*/
shaka.dash.ContentProtection.parseFromRepresentation = function(
elems, callback, context, ignoreDrmInfo) {
elems, callback, context, ignoreDrmInfo) {
const ContentProtection = shaka.dash.ContentProtection;
let repContext = ContentProtection.parseFromAdaptationSet(
elems, callback, ignoreDrmInfo);
elems, callback, ignoreDrmInfo);

if (context.firstRepresentation) {
let asUnknown = context.drmInfos.length == 1 &&
!context.drmInfos[0].keySystem;
!context.drmInfos[0].keySystem;
let asUnencrypted = context.drmInfos.length == 0;
let repUnencrypted = repContext.drmInfos.length == 0;

Expand All @@ -252,16 +252,165 @@ shaka.dash.ContentProtection.parseFromRepresentation = function(
// If we have filtered out all key-systems, throw an error.
if (context.drmInfos.length == 0) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_NO_COMMON_KEY_SYSTEM);
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_NO_COMMON_KEY_SYSTEM);
}
}

return repContext.defaultKeyId || context.defaultKeyId;
};


/**
* 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.findChild(element.node, 'ms:laurl');
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
if (mslaurlNode) {
return mslaurlNode.getAttribute('licenseUrl') || '';
}
return '';
};


/**
* @typedef {{
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
* type: number,
* length: number,
* value: string
* }}
*
* @description
* The parsed result of a PlayReady object record.
*
* @property {number} type
* Type of data stored in the record.
* @property {number} length
* Size of the record in bytes.
* @property {string} value
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
* 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,
};


/**
* @param {Uint16Array} recordData
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
* @param {number} recordCount
* @return {!Array.<shaka.dash.ContentProtection.PlayReadyRecord>}
* @private
*/
shaka.dash.ContentProtection.parseMsProRecords_ = function(
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
recordData, recordCount) {
let head = 0;
let records = [];
for (let i = 0; i < recordCount; i++) {
const recordRaw = recordData.subarray(head);
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
if (recordRaw.length > 1) {
const type = recordRaw[0];
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
const length = recordRaw[1];
const offset = 2;
const charCount = length / 2;
const end = charCount + offset;
// subarray end is exclusive
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
const rawValue = recordRaw.subarray(offset, end);
const value = shaka.util.StringUtils.fromUTF8(rawValue);
records.push({
type: type,
length: length,
value: value,
});
head = end;
}
}
return records;
};


/**
* Based off getLicenseServerURLFromInitData from dash.js
* https://github.com/Dash-Industry-Forum/dash.js
*
* @param {Uint16Array} bytes
* @return {!Array.<shaka.dash.ContentProtection.PlayReadyRecord>}
* @private
*/
shaka.dash.ContentProtection.parseMsPro_ = function(bytes) {
if (bytes.length < 2) {
return [];
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
}
const recordCount = bytes[2];
const recordData = bytes.subarray(3);
const ContentProtection = shaka.dash.ContentProtection;
return ContentProtection.parseMsProRecords_(recordData, recordCount);
};


/**
* @param {!Element} xml
* @return {string}
* @private
*/
shaka.dash.ContentProtection.getLaurl_ = function(xml) {
const laurlNodes = xml.getElementsByTagName('LA_URL');
if (laurlNodes && laurlNodes.length > 0) {
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
const laurlNode = laurlNodes[0];
if (laurlNode.hasChildNodes()) {
const laurl = laurlNode.childNodes[0].nodeValue;
if (laurl) {
return laurl;
}
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
}
}
return '';
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
};


/**
* Gets a PlayReady license URL from a content protection element
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
* containing a PlayReady Header Object
*
* @param {shaka.dash.ContentProtection.Element} element
* @return {string}
*/
shaka.dash.ContentProtection.getPlayReadyLicenseURL = function(element) {
const proNode = shaka.util.XmlUtils.findChild(element.node, 'mspr:pro');
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
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_(new Uint16Array(bytes.buffer));
const record = records.filter(function(record) {
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
return record.type === PLAYREADY_RECORD_TYPES.RIGHTS_MANAGEMENT;
}).pop();
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved

if (record) {
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
const parser = new DOMParser();
const xmlDocument = parser.parseFromString(record.value, 'application/xml');
const rootElement = xmlDocument.documentElement;
return ContentProtection.getLaurl_(rootElement);
} else {
return '';
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
}
};


/**
* Creates DrmInfo objects from the given element.
*
Expand All @@ -272,9 +421,10 @@ 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_;

/** @type {!Array.<shaka.extern.DrmInfo>} */
const out = [];
Expand All @@ -283,11 +433,23 @@ shaka.dash.ContentProtection.convertElements_ = function(
const keySystem = defaultKeySystems.get(element.schemeUri);
if (keySystem) {
goog.asserts.assert(
!element.init || element.init.length,
'Init data must be null or non-empty.');
!element.init || element.init.length,
'Init data must be null or non-empty.');

const initData = element.init || defaultInit;
const info = createDrmInfo(keySystem, initData);
const licenseParsers = {
tylerdaines marked this conversation as resolved.
Show resolved Hide resolved
'com.widevine.alpha':
ContentProtection.getWidevineLicenseUrl,
'com.microsoft.playready':
ContentProtection.getPlayReadyLicenseURL,
};

const licenseParser = licenseParsers[keySystem];
if (licenseParser) {
info.licenseServerUri = licenseParser(element);
}

out.push(info);
} else {
goog.asserts.assert(callback, 'ContentProtection callback is required');
Expand Down Expand Up @@ -341,11 +503,11 @@ shaka.dash.ContentProtection.parseElement_ = function(elem) {
let keyId = shaka.util.XmlUtils.getAttributeNS(elem, NS, 'default_KID');
/** @type {!Array.<string>} */
const psshs = shaka.util.XmlUtils.findChildrenNS(elem, NS, 'pssh')
.map(shaka.util.XmlUtils.getContents);
.map(shaka.util.XmlUtils.getContents);

if (!schemeUri) {
shaka.log.error('Missing required schemeIdUri attribute on',
'ContentProtection element', elem);
'ContentProtection element', elem);
return null;
}

Expand All @@ -354,9 +516,9 @@ shaka.dash.ContentProtection.parseElement_ = function(elem) {
keyId = keyId.replace(/-/g, '').toLowerCase();
if (keyId.includes(' ')) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_MULTIPLE_KEY_IDS_NOT_SUPPORTED);
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_MULTIPLE_KEY_IDS_NOT_SUPPORTED);
}
}

Expand All @@ -373,9 +535,9 @@ shaka.dash.ContentProtection.parseElement_ = function(elem) {
});
} catch (e) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_PSSH_BAD_ENCODING);
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_PSSH_BAD_ENCODING);
}

return {
Expand Down
Loading