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. * @const {string}
  100. * @private
  101. */
  102. shaka.dash.ContentProtection.CencNamespaceUri_ = 'urn:mpeg:cenc:2013';
  103. /**
  104. * Parses info from the ContentProtection elements at the AdaptationSet level.
  105. *
  106. * @param {!Array.<!Element>} elems
  107. * @param {shakaExtern.DashContentProtectionCallback} callback
  108. * @param {boolean} ignoreDrmInfo
  109. * @return {shaka.dash.ContentProtection.Context}
  110. */
  111. shaka.dash.ContentProtection.parseFromAdaptationSet = function(
  112. elems, callback, ignoreDrmInfo) {
  113. const ContentProtection = shaka.dash.ContentProtection;
  114. const Functional = shaka.util.Functional;
  115. const MapUtils = shaka.util.MapUtils;
  116. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  117. let parsed = ContentProtection.parseElements_(elems);
  118. /** @type {Array.<shakaExtern.InitDataOverride>} */
  119. let defaultInit = null;
  120. /** @type {!Array.<shakaExtern.DrmInfo>} */
  121. let drmInfos = [];
  122. let parsedNonCenc = [];
  123. // Get the default key ID; if there are multiple, they must all match.
  124. let keyIds = parsed.map(function(elem) { return elem.keyId; })
  125. .filter(Functional.isNotNull);
  126. if (keyIds.length) {
  127. if (keyIds.filter(Functional.isNotDuplicate).length > 1) {
  128. throw new shaka.util.Error(
  129. shaka.util.Error.Severity.CRITICAL,
  130. shaka.util.Error.Category.MANIFEST,
  131. shaka.util.Error.Code.DASH_CONFLICTING_KEY_IDS);
  132. }
  133. }
  134. if (!ignoreDrmInfo) {
  135. // Find the default key ID and init data. Create a new array of all the
  136. // non-CENC elements.
  137. parsedNonCenc = parsed.filter(function(elem) {
  138. if (elem.schemeUri == ContentProtection.MP4Protection_) {
  139. goog.asserts.assert(!elem.init || elem.init.length,
  140. 'Init data must be null or non-empty.');
  141. defaultInit = elem.init || defaultInit;
  142. return false;
  143. } else {
  144. return true;
  145. }
  146. });
  147. if (parsedNonCenc.length) {
  148. drmInfos = ContentProtection.convertElements_(
  149. defaultInit, callback, parsedNonCenc);
  150. // If there are no drmInfos after parsing, then add a dummy entry.
  151. // This may be removed in parseKeyIds.
  152. if (drmInfos.length == 0) {
  153. drmInfos = [ManifestParserUtils.createDrmInfo('', defaultInit)];
  154. }
  155. }
  156. }
  157. // If there are only CENC element(s) or ignoreDrmInfo flag is set, assume all
  158. // key-systems are supported.
  159. if (parsed.length && (ignoreDrmInfo || !parsedNonCenc.length)) {
  160. let keySystems = ContentProtection.defaultKeySystems_;
  161. drmInfos =
  162. MapUtils.values(keySystems)
  163. .filter(function(keySystem) {
  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. return keySystem != 'org.w3.clearkey';
  168. })
  169. .map(function(keySystem) {
  170. return ManifestParserUtils.createDrmInfo(keySystem, defaultInit);
  171. });
  172. }
  173. /** @type {?string} */
  174. let defaultKeyId = keyIds[0] || null;
  175. // Attach the default keyId, if it exists, to every initData.
  176. if (defaultKeyId) {
  177. drmInfos.forEach(function(drmInfo) {
  178. drmInfo.initData.forEach(function(initData) {
  179. initData.keyId = defaultKeyId;
  180. });
  181. });
  182. }
  183. return {
  184. defaultKeyId: defaultKeyId,
  185. defaultInit: defaultInit,
  186. drmInfos: drmInfos,
  187. firstRepresentation: true
  188. };
  189. };
  190. /**
  191. * Parses the given ContentProtection elements found at the Representation
  192. * level. This may update the |context|.
  193. *
  194. * @param {!Array.<!Element>} elems
  195. * @param {shakaExtern.DashContentProtectionCallback} callback
  196. * @param {shaka.dash.ContentProtection.Context} context
  197. * @param {boolean} ignoreDrmInfo
  198. * @return {?string} The parsed key ID
  199. */
  200. shaka.dash.ContentProtection.parseFromRepresentation = function(
  201. elems, callback, context, ignoreDrmInfo) {
  202. const ContentProtection = shaka.dash.ContentProtection;
  203. let repContext = ContentProtection.parseFromAdaptationSet(
  204. elems, callback, ignoreDrmInfo);
  205. if (context.firstRepresentation) {
  206. let asUnknown = context.drmInfos.length == 1 &&
  207. !context.drmInfos[0].keySystem;
  208. let asUnencrypted = context.drmInfos.length == 0;
  209. let repUnencrypted = repContext.drmInfos.length == 0;
  210. // There are two cases where we need to replace the |drmInfos| in the
  211. // context with those in the Representation:
  212. // 1. The AdaptationSet does not list any ContentProtection.
  213. // 2. The AdaptationSet only lists unknown key-systems.
  214. if (asUnencrypted || (asUnknown && !repUnencrypted)) {
  215. context.drmInfos = repContext.drmInfos;
  216. }
  217. context.firstRepresentation = false;
  218. } else if (repContext.drmInfos.length > 0) {
  219. // If this is not the first Representation, then we need to remove entries
  220. // from the context that do not appear in this Representation.
  221. context.drmInfos = context.drmInfos.filter(function(asInfo) {
  222. return repContext.drmInfos.some(function(repInfo) {
  223. return repInfo.keySystem == asInfo.keySystem;
  224. });
  225. });
  226. // If we have filtered out all key-systems, throw an error.
  227. if (context.drmInfos.length == 0) {
  228. throw new shaka.util.Error(
  229. shaka.util.Error.Severity.CRITICAL,
  230. shaka.util.Error.Category.MANIFEST,
  231. shaka.util.Error.Code.DASH_NO_COMMON_KEY_SYSTEM);
  232. }
  233. }
  234. return repContext.defaultKeyId || context.defaultKeyId;
  235. };
  236. /**
  237. * Creates DrmInfo objects from the given element.
  238. *
  239. * @param {Array.<shakaExtern.InitDataOverride>} defaultInit
  240. * @param {shakaExtern.DashContentProtectionCallback} callback
  241. * @param {!Array.<shaka.dash.ContentProtection.Element>} elements
  242. * @return {!Array.<shakaExtern.DrmInfo>}
  243. * @private
  244. */
  245. shaka.dash.ContentProtection.convertElements_ = function(
  246. defaultInit, callback, elements) {
  247. const Functional = shaka.util.Functional;
  248. return elements.map(
  249. /**
  250. * @param {shaka.dash.ContentProtection.Element} element
  251. * @return {!Array.<shakaExtern.DrmInfo>}
  252. */
  253. function(element) {
  254. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  255. const ContentProtection = shaka.dash.ContentProtection;
  256. let keySystem = ContentProtection.defaultKeySystems_[element.schemeUri];
  257. if (keySystem) {
  258. goog.asserts.assert(!element.init || element.init.length,
  259. 'Init data must be null or non-empty.');
  260. let initData = element.init || defaultInit;
  261. return [ManifestParserUtils.createDrmInfo(keySystem, initData)];
  262. } else {
  263. goog.asserts.assert(
  264. callback, 'ContentProtection callback is required');
  265. return callback(element.node) || [];
  266. }
  267. }).reduce(Functional.collapseArrays, []);
  268. };
  269. /**
  270. * Parses the given ContentProtection elements. If there is an error, it
  271. * removes those elements.
  272. *
  273. * @param {!Array.<!Element>} elems
  274. * @return {!Array.<shaka.dash.ContentProtection.Element>}
  275. * @private
  276. */
  277. shaka.dash.ContentProtection.parseElements_ = function(elems) {
  278. const Functional = shaka.util.Functional;
  279. return elems.map(
  280. /**
  281. * @param {!Element} elem
  282. * @return {?shaka.dash.ContentProtection.Element}
  283. */
  284. function(elem) {
  285. const NS = shaka.dash.ContentProtection.CencNamespaceUri_;
  286. /** @type {?string} */
  287. let schemeUri = elem.getAttribute('schemeIdUri');
  288. /** @type {?string} */
  289. let keyId = shaka.util.XmlUtils.getAttributeNS(elem, NS, 'default_KID');
  290. /** @type {!Array.<string>} */
  291. let psshs = shaka.util.XmlUtils.findChildrenNS(elem, NS, 'pssh')
  292. .map(shaka.util.XmlUtils.getContents);
  293. if (!schemeUri) {
  294. shaka.log.error('Missing required schemeIdUri attribute on',
  295. 'ContentProtection element', elem);
  296. return null;
  297. }
  298. schemeUri = schemeUri.toLowerCase();
  299. if (keyId) {
  300. keyId = keyId.replace(/-/g, '').toLowerCase();
  301. if (keyId.indexOf(' ') >= 0) {
  302. throw new shaka.util.Error(
  303. shaka.util.Error.Severity.CRITICAL,
  304. shaka.util.Error.Category.MANIFEST,
  305. shaka.util.Error.Code.DASH_MULTIPLE_KEY_IDS_NOT_SUPPORTED);
  306. }
  307. }
  308. /** @type {!Array.<shakaExtern.InitDataOverride>} */
  309. let init = [];
  310. try {
  311. init = psshs.map(function(pssh) {
  312. /** @type {shakaExtern.InitDataOverride} */
  313. let ret = {
  314. initDataType: 'cenc',
  315. initData: shaka.util.Uint8ArrayUtils.fromBase64(pssh),
  316. keyId: null
  317. };
  318. return ret;
  319. });
  320. } catch (e) {
  321. throw new shaka.util.Error(
  322. shaka.util.Error.Severity.CRITICAL,
  323. shaka.util.Error.Category.MANIFEST,
  324. shaka.util.Error.Code.DASH_PSSH_BAD_ENCODING);
  325. }
  326. /** @type {shaka.dash.ContentProtection.Element} */
  327. let element = {
  328. node: elem,
  329. schemeUri: schemeUri,
  330. keyId: keyId,
  331. init: (init.length > 0 ? init : null)
  332. };
  333. return element;
  334. }).filter(Functional.isNotNull);
  335. };