Source: lib/drm/playready.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.drm.PlayReady');
  7. goog.require('shaka.log');
  8. goog.require('shaka.util.BufferUtils');
  9. goog.require('shaka.util.StringUtils');
  10. goog.require('shaka.util.TXml');
  11. goog.require('shaka.util.Uint8ArrayUtils');
  12. /**
  13. * @summary A set of functions for parsing Microsoft Playready Objects.
  14. */
  15. shaka.drm.PlayReady = class {
  16. /**
  17. * Parses an Array buffer starting at byteOffset for PlayReady Object Records.
  18. * Each PRO Record is preceded by its PlayReady Record type and length in
  19. * bytes.
  20. *
  21. * PlayReady Object Record format: https://goo.gl/FTcu46
  22. *
  23. * @param {!DataView} view
  24. * @param {number} byteOffset
  25. * @return {!Array<shaka.drm.PlayReady.PlayReadyRecord>}
  26. * @private
  27. */
  28. static parseMsProRecords_(view, byteOffset) {
  29. const records = [];
  30. while (byteOffset < view.byteLength - 1) {
  31. const type = view.getUint16(byteOffset, true);
  32. byteOffset += 2;
  33. const byteLength = view.getUint16(byteOffset, true);
  34. byteOffset += 2;
  35. if ((byteLength & 1) != 0 || byteLength + byteOffset > view.byteLength) {
  36. shaka.log.warning('Malformed MS PRO object');
  37. return [];
  38. }
  39. const recordValue = shaka.util.BufferUtils.toUint8(
  40. view, byteOffset, byteLength);
  41. records.push({
  42. type: type,
  43. value: recordValue,
  44. });
  45. byteOffset += byteLength;
  46. }
  47. return records;
  48. }
  49. /**
  50. * Parses a buffer for PlayReady Objects. The data
  51. * should contain a 32-bit integer indicating the length of
  52. * the PRO in bytes. Following that, a 16-bit integer for
  53. * the number of PlayReady Object Records in the PRO. Lastly,
  54. * a byte array of the PRO Records themselves.
  55. *
  56. * PlayReady Object format: https://goo.gl/W8yAN4
  57. *
  58. * @param {BufferSource} data
  59. * @return {!Array<shaka.drm.PlayReady.PlayReadyRecord>}
  60. * @private
  61. */
  62. static parseMsPro_(data) {
  63. let byteOffset = 0;
  64. const view = shaka.util.BufferUtils.toDataView(data);
  65. // First 4 bytes is the PRO length (DWORD)
  66. const byteLength = view.getUint32(byteOffset, /* littleEndian= */ true);
  67. byteOffset += 4;
  68. if (byteLength != data.byteLength) {
  69. // Malformed PRO
  70. shaka.log.warning('PlayReady Object with invalid length encountered.');
  71. return [];
  72. }
  73. // Skip PRO Record count (WORD)
  74. byteOffset += 2;
  75. // Rest of the data contains the PRO Records
  76. return shaka.drm.PlayReady.parseMsProRecords_(view, byteOffset);
  77. }
  78. /**
  79. * PlayReady Header format: https://goo.gl/dBzxNA
  80. *
  81. * @param {!shaka.extern.xml.Node} xml
  82. * @return {string}
  83. * @private
  84. */
  85. static getLaurl_(xml) {
  86. const TXml = shaka.util.TXml;
  87. // LA_URL element is optional and no more than one is
  88. // allowed inside the DATA element. Only absolute URLs are allowed.
  89. // If the LA_URL element exists, it must not be empty.
  90. for (const elem of TXml.getElementsByTagName(xml, 'DATA')) {
  91. if (elem.children) {
  92. for (const child of elem.children) {
  93. if (child.tagName == 'LA_URL') {
  94. return /** @type {string} */(
  95. shaka.util.TXml.getTextContents(child));
  96. }
  97. }
  98. }
  99. }
  100. // Not found
  101. return '';
  102. }
  103. /**
  104. * Gets a PlayReady Header Object
  105. *
  106. * @param {!shaka.extern.xml.Node} element
  107. * @return {?shaka.extern.xml.Node}
  108. * @private
  109. */
  110. static getPlayReadyHeaderObject_(element) {
  111. const PLAYREADY_RECORD_TYPES = shaka.drm.PlayReady.PLAYREADY_RECORD_TYPES;
  112. const bytes = shaka.util.Uint8ArrayUtils.fromBase64(
  113. /** @type {string} */ (shaka.util.TXml.getTextContents(element)));
  114. const records = shaka.drm.PlayReady.parseMsPro_(bytes);
  115. const record = records.filter((record) => {
  116. return record.type === PLAYREADY_RECORD_TYPES.RIGHTS_MANAGEMENT;
  117. })[0];
  118. if (!record) {
  119. return null;
  120. }
  121. const xml = shaka.util.StringUtils.fromUTF16(record.value, true);
  122. const rootElement = shaka.util.TXml.parseXmlString(xml, 'WRMHEADER');
  123. if (!rootElement) {
  124. return null;
  125. }
  126. return rootElement;
  127. }
  128. /**
  129. * Gets a PlayReady license URL from a PlayReady Header Object
  130. *
  131. * @param {!shaka.extern.xml.Node} element
  132. * @return {string}
  133. */
  134. static getLicenseUrl(element) {
  135. try {
  136. const headerObject =
  137. shaka.drm.PlayReady.getPlayReadyHeaderObject_(element);
  138. if (!headerObject) {
  139. return '';
  140. }
  141. return shaka.drm.PlayReady.getLaurl_(headerObject);
  142. } catch (e) {
  143. return '';
  144. }
  145. }
  146. /**
  147. * Gets a PlayReady KID from a protection element containing a
  148. * PlayReady Header Object
  149. *
  150. * @param {!shaka.extern.xml.Node} element
  151. * @return {?string}
  152. */
  153. static getPlayReadyKID(element) {
  154. const rootElement = shaka.drm.PlayReady.getPlayReadyHeaderObject_(element);
  155. if (!rootElement) {
  156. return null;
  157. }
  158. const TXml = shaka.util.TXml;
  159. // KID element is optional and no more than one is
  160. // allowed inside the DATA element.
  161. for (const elem of TXml.getElementsByTagName(rootElement, 'DATA')) {
  162. const kid = TXml.findChild(elem, 'KID');
  163. if (kid) {
  164. // GUID: [DWORD, WORD, WORD, 8-BYTE]
  165. const guidBytes =
  166. shaka.util.Uint8ArrayUtils.fromBase64(
  167. /** @type {string} */ (shaka.util.TXml.getTextContents(kid)));
  168. // Reverse byte order from little-endian to big-endian
  169. const kidBytes = new Uint8Array([
  170. guidBytes[3], guidBytes[2], guidBytes[1], guidBytes[0],
  171. guidBytes[5], guidBytes[4],
  172. guidBytes[7], guidBytes[6],
  173. ...guidBytes.slice(8),
  174. ]);
  175. return shaka.util.Uint8ArrayUtils.toHex(kidBytes);
  176. }
  177. }
  178. // Not found
  179. return null;
  180. }
  181. };
  182. /**
  183. * @typedef {{
  184. * type: number,
  185. * value: !Uint8Array
  186. * }}
  187. *
  188. * @description
  189. * The parsed result of a PlayReady object record.
  190. *
  191. * @property {number} type
  192. * Type of data stored in the record.
  193. * @property {!Uint8Array} value
  194. * Record content.
  195. */
  196. shaka.drm.PlayReady.PlayReadyRecord;
  197. /**
  198. * Enum for PlayReady record types.
  199. * @enum {number}
  200. */
  201. shaka.drm.PlayReady.PLAYREADY_RECORD_TYPES = {
  202. RIGHTS_MANAGEMENT: 0x001,
  203. RESERVED: 0x002,
  204. EMBEDDED_LICENSE: 0x003,
  205. };