Source: lib/dash/content_protection.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.dash.ContentProtection');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.util.Error');
  21. goog.require('shaka.util.ManifestParserUtils');
  22. goog.require('shaka.util.Pssh');
  23. goog.require('shaka.util.Uint8ArrayUtils');
  24. goog.require('shaka.util.XmlUtils');
  25. /**
  26. * @namespace shaka.dash.ContentProtection
  27. * @summary A set of functions for parsing and interpreting ContentProtection
  28. * elements.
  29. */
  30. /**
  31. * @typedef {{
  32. * defaultKeyId: ?string,
  33. * defaultInit: Array.<shaka.extern.InitDataOverride>,
  34. * drmInfos: !Array.<shaka.extern.DrmInfo>,
  35. * firstRepresentation: boolean
  36. * }}
  37. *
  38. * @description
  39. * Contains information about the ContentProtection elements found at the
  40. * AdaptationSet level.
  41. *
  42. * @property {?string} defaultKeyId
  43. * The default key ID to use. This is used by parseKeyIds as a default. This
  44. * can be null to indicate that there is no default.
  45. * @property {Array.<shaka.extern.InitDataOverride>} defaultInit
  46. * The default init data override. This can be null to indicate that there
  47. * is no default.
  48. * @property {!Array.<shaka.extern.DrmInfo>} drmInfos
  49. * The DrmInfo objects.
  50. * @property {boolean} firstRepresentation
  51. * True when first parsed; changed to false after the first call to
  52. * parseKeyIds. This is used to determine if a dummy key-system should be
  53. * overwritten; namely that the first representation can replace the dummy
  54. * from the AdaptationSet.
  55. */
  56. shaka.dash.ContentProtection.Context;
  57. /**
  58. * @typedef {{
  59. * node: !Element,
  60. * schemeUri: string,
  61. * keyId: ?string,
  62. * init: Array.<shaka.extern.InitDataOverride>
  63. * }}
  64. *
  65. * @description
  66. * The parsed result of a single ContentProtection element.
  67. *
  68. * @property {!Element} node
  69. * The ContentProtection XML element.
  70. * @property {string} schemeUri
  71. * The scheme URI.
  72. * @property {?string} keyId
  73. * The default key ID, if present.
  74. * @property {Array.<shaka.extern.InitDataOverride>} init
  75. * The init data, if present. If there is no init data, it will be null. If
  76. * this is non-null, there is at least one element.
  77. */
  78. shaka.dash.ContentProtection.Element;
  79. /**
  80. * A map of scheme URI to key system name.
  81. *
  82. * @const {!Map.<string, string>}
  83. * @private
  84. */
  85. shaka.dash.ContentProtection.defaultKeySystems_ = new Map()
  86. .set('urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b',
  87. 'org.w3.clearkey')
  88. .set('urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
  89. 'com.widevine.alpha')
  90. .set('urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95',
  91. 'com.microsoft.playready')
  92. .set('urn:uuid:79f0049a-4098-8642-ab92-e65be0885f95',
  93. 'com.microsoft.playready')
  94. .set('urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb',
  95. 'com.adobe.primetime');
  96. /**
  97. * @const {string}
  98. * @private
  99. */
  100. shaka.dash.ContentProtection.MP4Protection_ =
  101. 'urn:mpeg:dash:mp4protection:2011';
  102. /**
  103. * @const {string}
  104. * @private
  105. */
  106. shaka.dash.ContentProtection.CencNamespaceUri_ = 'urn:mpeg:cenc:2013';
  107. /**
  108. * Parses info from the ContentProtection elements at the AdaptationSet level.
  109. *
  110. * @param {!Array.<!Element>} elems
  111. * @param {shaka.extern.DashContentProtectionCallback} callback
  112. * @param {boolean} ignoreDrmInfo
  113. * @return {shaka.dash.ContentProtection.Context}
  114. */
  115. shaka.dash.ContentProtection.parseFromAdaptationSet = function(
  116. elems, callback, ignoreDrmInfo) {
  117. const ContentProtection = shaka.dash.ContentProtection;
  118. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  119. let parsed = ContentProtection.parseElements_(elems);
  120. /** @type {Array.<shaka.extern.InitDataOverride>} */
  121. let defaultInit = null;
  122. /** @type {!Array.<shaka.extern.DrmInfo>} */
  123. let drmInfos = [];
  124. let parsedNonCenc = [];
  125. // Get the default key ID; if there are multiple, they must all match.
  126. const keyIds = new Set(parsed.map((element) => element.keyId));
  127. // Remove any possible null value (elements may have no key ids).
  128. keyIds.delete(null);
  129. if (keyIds.size > 1) {
  130. throw new shaka.util.Error(
  131. shaka.util.Error.Severity.CRITICAL,
  132. shaka.util.Error.Category.MANIFEST,
  133. shaka.util.Error.Code.DASH_CONFLICTING_KEY_IDS);
  134. }
  135. if (!ignoreDrmInfo) {
  136. // Find the default key ID and init data. Create a new array of all the
  137. // non-CENC elements.
  138. parsedNonCenc = parsed.filter(function(elem) {
  139. if (elem.schemeUri == ContentProtection.MP4Protection_) {
  140. goog.asserts.assert(!elem.init || elem.init.length,
  141. 'Init data must be null or non-empty.');
  142. defaultInit = elem.init || defaultInit;
  143. return false;
  144. } else {
  145. return true;
  146. }
  147. });
  148. if (parsedNonCenc.length) {
  149. drmInfos = ContentProtection.convertElements_(
  150. defaultInit, callback, parsedNonCenc);
  151. // If there are no drmInfos after parsing, then add a dummy entry.
  152. // This may be removed in parseKeyIds.
  153. if (drmInfos.length == 0) {
  154. drmInfos = [ManifestParserUtils.createDrmInfo('', defaultInit)];
  155. }
  156. }
  157. }
  158. // If there are only CENC element(s) or ignoreDrmInfo flag is set, assume all
  159. // key-systems are supported.
  160. if (parsed.length && (ignoreDrmInfo || !parsedNonCenc.length)) {
  161. drmInfos = [];
  162. const keySystems = ContentProtection.defaultKeySystems_;
  163. for (const keySystem of keySystems.values()) {
  164. // If the manifest doesn't specify any key systems, we shouldn't
  165. // put clearkey in this list. Otherwise, it may be triggered when
  166. // a real key system should be used instead.
  167. if (keySystem != 'org.w3.clearkey') {
  168. const info = ManifestParserUtils.createDrmInfo(keySystem, defaultInit);
  169. drmInfos.push(info);
  170. }
  171. }
  172. }
  173. // If we have a default key id, apply it to every initData.
  174. const defaultKeyId = Array.from(keyIds)[0] || null;
  175. if (defaultKeyId) {
  176. for (const info of drmInfos) {
  177. for (const initData of info.initData) {
  178. initData.keyId = defaultKeyId;
  179. }
  180. }
  181. }
  182. return {
  183. defaultKeyId: defaultKeyId,
  184. defaultInit: defaultInit,
  185. drmInfos: drmInfos,
  186. firstRepresentation: true,
  187. };
  188. };
  189. /**
  190. * Parses the given ContentProtection elements found at the Representation
  191. * level. This may update the |context|.
  192. *
  193. * @param {!Array.<!Element>} elems
  194. * @param {shaka.extern.DashContentProtectionCallback} callback
  195. * @param {shaka.dash.ContentProtection.Context} context
  196. * @param {boolean} ignoreDrmInfo
  197. * @return {?string} The parsed key ID
  198. */
  199. shaka.dash.ContentProtection.parseFromRepresentation = function(
  200. elems, callback, context, ignoreDrmInfo) {
  201. const ContentProtection = shaka.dash.ContentProtection;
  202. let repContext = ContentProtection.parseFromAdaptationSet(
  203. elems, callback, ignoreDrmInfo);
  204. if (context.firstRepresentation) {
  205. let asUnknown = context.drmInfos.length == 1 &&
  206. !context.drmInfos[0].keySystem;
  207. let asUnencrypted = context.drmInfos.length == 0;
  208. let repUnencrypted = repContext.drmInfos.length == 0;
  209. // There are two cases where we need to replace the |drmInfos| in the
  210. // context with those in the Representation:
  211. // 1. The AdaptationSet does not list any ContentProtection.
  212. // 2. The AdaptationSet only lists unknown key-systems.
  213. if (asUnencrypted || (asUnknown && !repUnencrypted)) {
  214. context.drmInfos = repContext.drmInfos;
  215. }
  216. context.firstRepresentation = false;
  217. } else if (repContext.drmInfos.length > 0) {
  218. // If this is not the first Representation, then we need to remove entries
  219. // from the context that do not appear in this Representation.
  220. context.drmInfos = context.drmInfos.filter(function(asInfo) {
  221. return repContext.drmInfos.some(function(repInfo) {
  222. return repInfo.keySystem == asInfo.keySystem;
  223. });
  224. });
  225. // If we have filtered out all key-systems, throw an error.
  226. if (context.drmInfos.length == 0) {
  227. throw new shaka.util.Error(
  228. shaka.util.Error.Severity.CRITICAL,
  229. shaka.util.Error.Category.MANIFEST,
  230. shaka.util.Error.Code.DASH_NO_COMMON_KEY_SYSTEM);
  231. }
  232. }
  233. return repContext.defaultKeyId || context.defaultKeyId;
  234. };
  235. /**
  236. * Gets a Widevine license URL from a content protection element
  237. * containing a custom `ms:laurl` element
  238. *
  239. * @param {shaka.dash.ContentProtection.Element} element
  240. * @return {string}
  241. */
  242. shaka.dash.ContentProtection.getWidevineLicenseUrl = function(element) {
  243. const mslaurlNode = shaka.util.XmlUtils.findChildNS(
  244. element.node, 'urn:microsoft', 'laurl');
  245. if (mslaurlNode) {
  246. return mslaurlNode.getAttribute('licenseUrl') || '';
  247. }
  248. return '';
  249. };
  250. /**
  251. * @typedef {{
  252. * type: number,
  253. * value: !Uint8Array
  254. * }}
  255. *
  256. * @description
  257. * The parsed result of a PlayReady object record.
  258. *
  259. * @property {number} type
  260. * Type of data stored in the record.
  261. * @property {!Uint8Array} value
  262. * Record content.
  263. */
  264. shaka.dash.ContentProtection.PlayReadyRecord;
  265. /**
  266. * Enum for PlayReady record types.
  267. * @enum {number}
  268. */
  269. shaka.dash.ContentProtection.PLAYREADY_RECORD_TYPES = {
  270. RIGHTS_MANAGEMENT: 0x001,
  271. RESERVED: 0x002,
  272. EMBEDDED_LICENSE: 0x003,
  273. };
  274. /**
  275. * Parses an Array buffer starting at byteOffset for PlayReady Object Records.
  276. * Each PRO Record is preceded by its PlayReady Record type and length in bytes.
  277. *
  278. * PlayReady Object Record format: https://goo.gl/FTcu46
  279. *
  280. * @param {!ArrayBuffer} recordData
  281. * @param {number} byteOffset
  282. * @return {!Array.<shaka.dash.ContentProtection.PlayReadyRecord>}
  283. * @private
  284. */
  285. shaka.dash.ContentProtection.parseMsProRecords_ = function(
  286. recordData, byteOffset) {
  287. const records = [];
  288. const view = new DataView(recordData);
  289. while (byteOffset < recordData.byteLength - 1) {
  290. const type = view.getUint16(byteOffset, true);
  291. byteOffset += 2;
  292. const byteLength = view.getUint16(byteOffset, true);
  293. byteOffset += 2;
  294. goog.asserts.assert(
  295. (byteLength & 1) === 0,
  296. 'expected byteLength to be an even number');
  297. const recordValue = new Uint8Array(recordData, byteOffset, byteLength);
  298. records.push({
  299. type: type,
  300. value: recordValue,
  301. });
  302. byteOffset += byteLength;
  303. }
  304. return records;
  305. };
  306. /**
  307. * Parses an ArrayBuffer for PlayReady Objects. The data
  308. * should contain a 32-bit integer indicating the length of
  309. * the PRO in bytes. Following that, a 16-bit integer for
  310. * the number of PlayReady Object Records in the PRO. Lastly,
  311. * a byte array of the PRO Records themselves.
  312. *
  313. * PlayReady Object format: https://goo.gl/W8yAN4
  314. *
  315. * @param {!ArrayBuffer} data
  316. * @return {!Array.<shaka.dash.ContentProtection.PlayReadyRecord>}
  317. * @private
  318. */
  319. shaka.dash.ContentProtection.parseMsPro_ = function(data) {
  320. let byteOffset = 0;
  321. const view = new DataView(data);
  322. // First 4 bytes is the PRO length (DWORD)
  323. const byteLength = view.getUint32(byteOffset, true /* littleEndian */);
  324. byteOffset += 4;
  325. if (byteLength !== data.byteLength) {
  326. // Malformed PRO
  327. shaka.log.warning('PlayReady Object with invalid length encountered.');
  328. return [];
  329. }
  330. // Skip PRO Record count (WORD)
  331. byteOffset += 2;
  332. // Rest of the data contains the PRO Records
  333. const ContentProtection = shaka.dash.ContentProtection;
  334. return ContentProtection.parseMsProRecords_(data, byteOffset);
  335. };
  336. /**
  337. * PlayReady Header format: https://goo.gl/dBzxNA
  338. *
  339. * @param {!Element} xml
  340. * @return {string}
  341. * @private
  342. */
  343. shaka.dash.ContentProtection.getLaurl_ = function(xml) {
  344. // LA_URL element is optional and no more than one is
  345. // allowed inside the DATA element. Only absolute URLs are allowed.
  346. // If the LA_URL element exists, it must not be empty.
  347. for (const elem of xml.getElementsByTagName('DATA')) {
  348. for (const child of elem.childNodes) {
  349. if (child instanceof Element && child.tagName == 'LA_URL') {
  350. return child.textContent;
  351. }
  352. }
  353. }
  354. // Not found
  355. return '';
  356. };
  357. /**
  358. * Gets a PlayReady license URL from a content protection element
  359. * containing a PlayReady Header Object
  360. *
  361. * @param {shaka.dash.ContentProtection.Element} element
  362. * @return {string}
  363. */
  364. shaka.dash.ContentProtection.getPlayReadyLicenseUrl = function(element) {
  365. const proNode = shaka.util.XmlUtils.findChildNS(
  366. element.node, 'urn:microsoft:playready', 'pro');
  367. if (!proNode) {
  368. return '';
  369. }
  370. const ContentProtection = shaka.dash.ContentProtection;
  371. const PLAYREADY_RECORD_TYPES = ContentProtection.PLAYREADY_RECORD_TYPES;
  372. const bytes = shaka.util.Uint8ArrayUtils.fromBase64(proNode.textContent);
  373. const records = ContentProtection.parseMsPro_(bytes.buffer);
  374. const record = records.filter((record) => {
  375. return record.type === PLAYREADY_RECORD_TYPES.RIGHTS_MANAGEMENT;
  376. })[0];
  377. if (!record) {
  378. return '';
  379. }
  380. const xml = shaka.util.StringUtils.fromUTF16(record.value, true);
  381. const rootElement = shaka.util.XmlUtils.parseXmlString(xml, 'WRMHEADER');
  382. if (!rootElement) {
  383. return '';
  384. }
  385. return ContentProtection.getLaurl_(rootElement);
  386. };
  387. /**
  388. * Gets a PlayReady initData from a content protection element
  389. * containing a PlayReady Pro Object
  390. *
  391. * @param {shaka.dash.ContentProtection.Element} element
  392. * @return {?Array.<shaka.extern.InitDataOverride>}
  393. * @private
  394. */
  395. shaka.dash.ContentProtection.getInitDataFromPro_ = function(element) {
  396. const proNode = shaka.util.XmlUtils.findChildNS(
  397. element.node, 'urn:microsoft:playready', 'pro');
  398. if (!proNode) {
  399. return null;
  400. }
  401. const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
  402. const data = Uint8ArrayUtils.fromBase64(proNode.textContent);
  403. const systemId = new Uint8Array([
  404. 0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86,
  405. 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95,
  406. ]);
  407. const pssh = shaka.util.Pssh.createPssh(data, systemId);
  408. return [
  409. {
  410. initData: pssh,
  411. initDataType: 'cenc',
  412. keyId: element.keyId,
  413. },
  414. ];
  415. };
  416. /**
  417. * Creates DrmInfo objects from the given element.
  418. *
  419. * @param {Array.<shaka.extern.InitDataOverride>} defaultInit
  420. * @param {shaka.extern.DashContentProtectionCallback} callback
  421. * @param {!Array.<shaka.dash.ContentProtection.Element>} elements
  422. * @return {!Array.<shaka.extern.DrmInfo>}
  423. * @private
  424. */
  425. shaka.dash.ContentProtection.convertElements_ = function(
  426. defaultInit, callback, elements) {
  427. const ContentProtection = shaka.dash.ContentProtection;
  428. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  429. const defaultKeySystems = ContentProtection.defaultKeySystems_;
  430. const licenseUrlParsers = ContentProtection.licenseUrlParsers_;
  431. /** @type {!Array.<shaka.extern.DrmInfo>} */
  432. const out = [];
  433. for (const element of elements) {
  434. const keySystem = defaultKeySystems.get(element.schemeUri);
  435. if (keySystem) {
  436. goog.asserts.assert(
  437. !element.init || element.init.length,
  438. 'Init data must be null or non-empty.');
  439. const proInitData = ContentProtection.getInitDataFromPro_(element);
  440. const initData = element.init || defaultInit || proInitData;
  441. const info = ManifestParserUtils.createDrmInfo(keySystem, initData);
  442. const licenseParser = licenseUrlParsers.get(keySystem);
  443. if (licenseParser) {
  444. info.licenseServerUri = licenseParser(element);
  445. }
  446. out.push(info);
  447. } else {
  448. goog.asserts.assert(callback, 'ContentProtection callback is required');
  449. const infos = callback(element.node) || [];
  450. for (const info of infos) {
  451. out.push(info);
  452. }
  453. }
  454. }
  455. return out;
  456. };
  457. /**
  458. * A map of key system name to license server url parser.
  459. *
  460. * @const {!Map.<string, function(shaka.dash.ContentProtection.Element)>}
  461. * @private
  462. */
  463. shaka.dash.ContentProtection.licenseUrlParsers_ = new Map()
  464. .set('com.widevine.alpha',
  465. shaka.dash.ContentProtection.getWidevineLicenseUrl)
  466. .set('com.microsoft.playready',
  467. shaka.dash.ContentProtection.getPlayReadyLicenseUrl);
  468. /**
  469. * Parses the given ContentProtection elements. If there is an error, it
  470. * removes those elements.
  471. *
  472. * @param {!Array.<!Element>} elems
  473. * @return {!Array.<shaka.dash.ContentProtection.Element>}
  474. * @private
  475. */
  476. shaka.dash.ContentProtection.parseElements_ = function(elems) {
  477. /** @type {!Array.<shaka.dash.ContentProtection.Element>} */
  478. const out = [];
  479. for (const elem of elems) {
  480. const parsed = shaka.dash.ContentProtection.parseElement_(elem);
  481. if (parsed) {
  482. out.push(parsed);
  483. }
  484. }
  485. return out;
  486. };
  487. /**
  488. * Parses the given ContentProtection element.
  489. *
  490. * @param {!Element} elem
  491. * @return {?shaka.dash.ContentProtection.Element}
  492. * @private
  493. */
  494. shaka.dash.ContentProtection.parseElement_ = function(elem) {
  495. const NS = shaka.dash.ContentProtection.CencNamespaceUri_;
  496. /** @type {?string} */
  497. let schemeUri = elem.getAttribute('schemeIdUri');
  498. /** @type {?string} */
  499. let keyId = shaka.util.XmlUtils.getAttributeNS(elem, NS, 'default_KID');
  500. /** @type {!Array.<string>} */
  501. const psshs = shaka.util.XmlUtils.findChildrenNS(elem, NS, 'pssh')
  502. .map(shaka.util.XmlUtils.getContents);
  503. if (!schemeUri) {
  504. shaka.log.error('Missing required schemeIdUri attribute on',
  505. 'ContentProtection element', elem);
  506. return null;
  507. }
  508. schemeUri = schemeUri.toLowerCase();
  509. if (keyId) {
  510. keyId = keyId.replace(/-/g, '').toLowerCase();
  511. if (keyId.includes(' ')) {
  512. throw new shaka.util.Error(
  513. shaka.util.Error.Severity.CRITICAL,
  514. shaka.util.Error.Category.MANIFEST,
  515. shaka.util.Error.Code.DASH_MULTIPLE_KEY_IDS_NOT_SUPPORTED);
  516. }
  517. }
  518. /** @type {!Array.<shaka.extern.InitDataOverride>} */
  519. let init = [];
  520. try {
  521. // Try parsing PSSH data.
  522. init = psshs.map((pssh) => {
  523. return {
  524. initDataType: 'cenc',
  525. initData: shaka.util.Uint8ArrayUtils.fromBase64(pssh),
  526. keyId: null,
  527. };
  528. });
  529. } catch (e) {
  530. throw new shaka.util.Error(
  531. shaka.util.Error.Severity.CRITICAL,
  532. shaka.util.Error.Category.MANIFEST,
  533. shaka.util.Error.Code.DASH_PSSH_BAD_ENCODING);
  534. }
  535. return {
  536. node: elem,
  537. schemeUri: schemeUri,
  538. keyId: keyId,
  539. init: (init.length > 0 ? init : null),
  540. };
  541. };