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.Functional');
  22. goog.require('shaka.util.ManifestParserUtils');
  23. goog.require('shaka.util.MapUtils');
  24. goog.require('shaka.util.Uint8ArrayUtils');
  25. goog.require('shaka.util.XmlUtils');
  26. /**
  27. * @namespace shaka.dash.ContentProtection
  28. * @summary A set of functions for parsing and interpreting ContentProtection
  29. * elements.
  30. */
  31. /**
  32. * @typedef {{
  33. * defaultKeyId: ?string,
  34. * defaultInit: Array.<shakaExtern.InitDataOverride>,
  35. * drmInfos: !Array.<shakaExtern.DrmInfo>,
  36. * firstRepresentation: boolean
  37. * }}
  38. *
  39. * @description
  40. * Contains information about the ContentProtection elements found at the
  41. * AdaptationSet level.
  42. *
  43. * @property {?string} defaultKeyId
  44. * The default key ID to use. This is used by parseKeyIds as a default. This
  45. * can be null to indicate that there is no default.
  46. * @property {Array.<shakaExtern.InitDataOverride>} defaultInit
  47. * The default init data override. This can be null to indicate that there
  48. * is no default.
  49. * @property {!Array.<shakaExtern.DrmInfo>} drmInfos
  50. * The DrmInfo objects.
  51. * @property {boolean} firstRepresentation
  52. * True when first parsed; changed to false after the first call to
  53. * parseKeyIds. This is used to determine if a dummy key-system should be
  54. * overwritten; namely that the first representation can replace the dummy
  55. * from the AdaptationSet.
  56. */
  57. shaka.dash.ContentProtection.Context;
  58. /**
  59. * @typedef {{
  60. * node: !Element,
  61. * schemeUri: string,
  62. * keyId: ?string,
  63. * init: Array.<shakaExtern.InitDataOverride>
  64. * }}
  65. *
  66. * @description
  67. * The parsed result of a single ContentProtection element.
  68. *
  69. * @property {!Element} node
  70. * The ContentProtection XML element.
  71. * @property {string} schemeUri
  72. * The scheme URI.
  73. * @property {?string} keyId
  74. * The default key ID, if present.
  75. * @property {Array.<shakaExtern.InitDataOverride>} init
  76. * The init data, if present. If there is no init data, it will be null. If
  77. * this is non-null, there is at least one element.
  78. */
  79. shaka.dash.ContentProtection.Element;
  80. /**
  81. * A map of scheme URI to key system name.
  82. *
  83. * @const {!Object.<string, string>}
  84. * @private
  85. */
  86. shaka.dash.ContentProtection.defaultKeySystems_ = {
  87. 'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey',
  88. 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha',
  89. 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready',
  90. 'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime'
  91. };
  92. /**
  93. * @const {string}
  94. * @private
  95. */
  96. shaka.dash.ContentProtection.MP4Protection_ =
  97. 'urn:mpeg:dash:mp4protection:2011';
  98. /**
  99. * Parses info from the ContentProtection elements at the AdaptationSet level.
  100. *
  101. * @param {!Array.<!Element>} elems
  102. * @param {shakaExtern.DashContentProtectionCallback} callback
  103. * @param {boolean} ignoreDrmInfo
  104. * @return {shaka.dash.ContentProtection.Context}
  105. */
  106. shaka.dash.ContentProtection.parseFromAdaptationSet = function(
  107. elems, callback, ignoreDrmInfo) {
  108. var ContentProtection = shaka.dash.ContentProtection;
  109. var Functional = shaka.util.Functional;
  110. var MapUtils = shaka.util.MapUtils;
  111. var ManifestParserUtils = shaka.util.ManifestParserUtils;
  112. var parsed = ContentProtection.parseElements_(elems);
  113. /** @type {Array.<shakaExtern.InitDataOverride>} */
  114. var defaultInit = null;
  115. /** @type {!Array.<shakaExtern.DrmInfo>} */
  116. var drmInfos = [];
  117. var parsedNonCenc = [];
  118. // Get the default key ID; if there are multiple, they must all match.
  119. var keyIds = parsed.map(function(elem) { return elem.keyId; })
  120. .filter(Functional.isNotNull);
  121. if (keyIds.length) {
  122. if (keyIds.filter(Functional.isNotDuplicate).length > 1) {
  123. throw new shaka.util.Error(
  124. shaka.util.Error.Severity.CRITICAL,
  125. shaka.util.Error.Category.MANIFEST,
  126. shaka.util.Error.Code.DASH_CONFLICTING_KEY_IDS);
  127. }
  128. }
  129. if (!ignoreDrmInfo) {
  130. // Find the default key ID and init data. Create a new array of all the
  131. // non-CENC elements.
  132. parsedNonCenc = parsed.filter(function(elem) {
  133. if (elem.schemeUri == ContentProtection.MP4Protection_) {
  134. goog.asserts.assert(!elem.init || elem.init.length,
  135. 'Init data must be null or non-empty.');
  136. defaultInit = elem.init || defaultInit;
  137. return false;
  138. } else {
  139. return true;
  140. }
  141. });
  142. if (parsedNonCenc.length) {
  143. drmInfos = ContentProtection.convertElements_(
  144. defaultInit, callback, parsedNonCenc);
  145. // If there are no drmInfos after parsing, then add a dummy entry.
  146. // This may be removed in parseKeyIds.
  147. if (drmInfos.length == 0) {
  148. drmInfos = [ManifestParserUtils.createDrmInfo('', defaultInit)];
  149. }
  150. }
  151. }
  152. // If there are only CENC element(s) or ignoreDrmInfo flag is set, assume all
  153. // key-systems are supported.
  154. if (parsed.length && (ignoreDrmInfo || !parsedNonCenc.length)) {
  155. var keySystems = ContentProtection.defaultKeySystems_;
  156. drmInfos =
  157. MapUtils.values(keySystems)
  158. .map(function(keySystem) {
  159. return ManifestParserUtils.createDrmInfo(keySystem, defaultInit);
  160. });
  161. }
  162. /** @type {?string} */
  163. var defaultKeyId = keyIds[0] || null;
  164. // attach the default keyId, if it exists, to every initData
  165. if (defaultKeyId) {
  166. drmInfos.forEach(function(drmInfo) {
  167. drmInfo.initData.forEach(function(initData) {
  168. initData.keyId = defaultKeyId;
  169. });
  170. });
  171. }
  172. return {
  173. defaultKeyId: defaultKeyId,
  174. defaultInit: defaultInit,
  175. drmInfos: drmInfos,
  176. firstRepresentation: true
  177. };
  178. };
  179. /**
  180. * Parses the given ContentProtection elements found at the Representation
  181. * level. This may update the |context|.
  182. *
  183. * @param {!Array.<!Element>} elems
  184. * @param {shakaExtern.DashContentProtectionCallback} callback
  185. * @param {shaka.dash.ContentProtection.Context} context
  186. * @param {boolean} ignoreDrmInfo
  187. * @return {?string} The parsed key ID
  188. */
  189. shaka.dash.ContentProtection.parseFromRepresentation = function(
  190. elems, callback, context, ignoreDrmInfo) {
  191. var ContentProtection = shaka.dash.ContentProtection;
  192. var repContext = ContentProtection.parseFromAdaptationSet(
  193. elems, callback, ignoreDrmInfo);
  194. if (context.firstRepresentation) {
  195. var asUnknown = context.drmInfos.length == 1 &&
  196. !context.drmInfos[0].keySystem;
  197. var asUnencrypted = context.drmInfos.length == 0;
  198. var repUnencrypted = repContext.drmInfos.length == 0;
  199. // There are two cases when we need to replace the |drmInfos| in the context
  200. // with those in the Representation:
  201. // * The AdaptationSet does not list any ContentProtection.
  202. // * The AdaptationSet only lists unknown key-systems.
  203. if (asUnencrypted || (asUnknown && !repUnencrypted)) {
  204. context.drmInfos = repContext.drmInfos;
  205. }
  206. context.firstRepresentation = false;
  207. } else if (repContext.drmInfos.length > 0) {
  208. // If this is not the first Representation, then we need to remove entries
  209. // from the context that do not appear in this Representation.
  210. context.drmInfos = context.drmInfos.filter(function(asInfo) {
  211. return repContext.drmInfos.some(function(repInfo) {
  212. return repInfo.keySystem == asInfo.keySystem;
  213. });
  214. });
  215. // If we have filtered out all key-systems, throw an error.
  216. if (context.drmInfos.length == 0) {
  217. throw new shaka.util.Error(
  218. shaka.util.Error.Severity.CRITICAL,
  219. shaka.util.Error.Category.MANIFEST,
  220. shaka.util.Error.Code.DASH_NO_COMMON_KEY_SYSTEM);
  221. }
  222. }
  223. return repContext.defaultKeyId || context.defaultKeyId;
  224. };
  225. /**
  226. * Creates DrmInfo objects from the given element.
  227. *
  228. * @param {Array.<shakaExtern.InitDataOverride>} defaultInit
  229. * @param {shakaExtern.DashContentProtectionCallback} callback
  230. * @param {!Array.<shaka.dash.ContentProtection.Element>} elements
  231. * @return {!Array.<shakaExtern.DrmInfo>}
  232. * @private
  233. */
  234. shaka.dash.ContentProtection.convertElements_ = function(
  235. defaultInit, callback, elements) {
  236. var Functional = shaka.util.Functional;
  237. return elements.map(
  238. /**
  239. * @param {shaka.dash.ContentProtection.Element} element
  240. * @return {!Array.<shakaExtern.DrmInfo>}
  241. */
  242. function(element) {
  243. var ManifestParserUtils = shaka.util.ManifestParserUtils;
  244. var ContentProtection = shaka.dash.ContentProtection;
  245. var keySystem = ContentProtection.defaultKeySystems_[element.schemeUri];
  246. if (keySystem) {
  247. goog.asserts.assert(!element.init || element.init.length,
  248. 'Init data must be null or non-empty.');
  249. var initData = element.init || defaultInit;
  250. return [ManifestParserUtils.createDrmInfo(keySystem, initData)];
  251. } else {
  252. goog.asserts.assert(
  253. callback, 'ContentProtection callback is required');
  254. return callback(element.node) || [];
  255. }
  256. }).reduce(Functional.collapseArrays, []);
  257. };
  258. /**
  259. * Parses the given ContentProtection elements. If there is an error, it
  260. * removes those elements.
  261. *
  262. * @param {!Array.<!Element>} elems
  263. * @return {!Array.<shaka.dash.ContentProtection.Element>}
  264. * @private
  265. */
  266. shaka.dash.ContentProtection.parseElements_ = function(elems) {
  267. var Functional = shaka.util.Functional;
  268. return elems.map(
  269. /**
  270. * @param {!Element} elem
  271. * @return {?shaka.dash.ContentProtection.Element}
  272. */
  273. function(elem) {
  274. /** @type {?string} */
  275. var schemeUri = elem.getAttribute('schemeIdUri');
  276. /** @type {?string} */
  277. var keyId = elem.getAttribute('cenc:default_KID');
  278. /** @type {!Array.<string>} */
  279. var psshs = shaka.util.XmlUtils.findChildren(elem, 'cenc:pssh')
  280. .map(shaka.util.XmlUtils.getContents);
  281. if (!schemeUri) {
  282. shaka.log.error('Missing required schemeIdUri attribute on',
  283. 'ContentProtection element', elem);
  284. return null;
  285. }
  286. schemeUri = schemeUri.toLowerCase();
  287. if (keyId) {
  288. keyId = keyId.replace(/-/g, '').toLowerCase();
  289. if (keyId.indexOf(' ') >= 0) {
  290. throw new shaka.util.Error(
  291. shaka.util.Error.Severity.CRITICAL,
  292. shaka.util.Error.Category.MANIFEST,
  293. shaka.util.Error.Code.DASH_MULTIPLE_KEY_IDS_NOT_SUPPORTED);
  294. }
  295. }
  296. /** @type {!Array.<shakaExtern.InitDataOverride>} */
  297. var init = [];
  298. try {
  299. init = psshs.map(function(pssh) {
  300. /** @type {shakaExtern.InitDataOverride} */
  301. var ret = {
  302. initDataType: 'cenc',
  303. initData: shaka.util.Uint8ArrayUtils.fromBase64(pssh),
  304. keyId: null
  305. };
  306. return ret;
  307. });
  308. } catch (e) {
  309. throw new shaka.util.Error(
  310. shaka.util.Error.Severity.CRITICAL,
  311. shaka.util.Error.Category.MANIFEST,
  312. shaka.util.Error.Code.DASH_PSSH_BAD_ENCODING);
  313. }
  314. /** @type {shaka.dash.ContentProtection.Element} */
  315. var element = {
  316. node: elem,
  317. schemeUri: schemeUri,
  318. keyId: keyId,
  319. init: (init.length > 0 ? init : null)
  320. };
  321. return element;
  322. }).filter(Functional.isNotNull);
  323. };