Source: lib/util/pssh.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.util.Pssh');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.util.BufferUtils');
  10. goog.require('shaka.util.Mp4Parser');
  11. goog.require('shaka.util.Uint8ArrayUtils');
  12. /**
  13. * @summary
  14. * Parse a PSSH box and extract the system IDs.
  15. */
  16. shaka.util.Pssh = class {
  17. /**
  18. * @param {!Uint8Array} psshBox
  19. */
  20. constructor(psshBox) {
  21. /**
  22. * In hex.
  23. * @type {!Array<string>}
  24. */
  25. this.systemIds = [];
  26. /**
  27. * In hex.
  28. * @type {!Array<string>}
  29. */
  30. this.cencKeyIds = [];
  31. /**
  32. * Array with the pssh boxes found.
  33. * @type {!Array<!Uint8Array>}
  34. */
  35. this.data = [];
  36. new shaka.util.Mp4Parser()
  37. .box('moov', shaka.util.Mp4Parser.children)
  38. .box('moof', shaka.util.Mp4Parser.children)
  39. .fullBox('pssh', (box) => this.parsePsshBox_(box))
  40. .parse(psshBox);
  41. if (this.data.length == 0) {
  42. shaka.log.v2('No pssh box found!');
  43. }
  44. }
  45. /**
  46. * @param {!shaka.extern.ParsedBox} box
  47. * @private
  48. */
  49. parsePsshBox_(box) {
  50. goog.asserts.assert(
  51. box.version != null,
  52. 'PSSH boxes are full boxes and must have a valid version');
  53. goog.asserts.assert(
  54. box.flags != null,
  55. 'PSSH boxes are full boxes and must have a valid flag');
  56. if (box.version > 1) {
  57. shaka.log.warning('Unrecognized PSSH version found!');
  58. return;
  59. }
  60. // The "reader" gives us a view on the payload of the box. Create a new
  61. // view that contains the whole box.
  62. const dataView = box.reader.getDataView();
  63. goog.asserts.assert(
  64. dataView.byteOffset >= 12, 'DataView at incorrect position');
  65. const pssh = shaka.util.BufferUtils.toUint8(dataView, -12, box.size);
  66. this.data.push(pssh);
  67. this.systemIds.push(
  68. shaka.util.Uint8ArrayUtils.toHex(box.reader.readBytes(16)));
  69. if (box.version > 0) {
  70. const numKeyIds = box.reader.readUint32();
  71. for (let i = 0; i < numKeyIds; i++) {
  72. const keyId =
  73. shaka.util.Uint8ArrayUtils.toHex(box.reader.readBytes(16));
  74. this.cencKeyIds.push(keyId);
  75. }
  76. }
  77. }
  78. /**
  79. * Creates a pssh blob from the given system ID, data, keyIds and version.
  80. *
  81. * @param {!Uint8Array} data
  82. * @param {!Uint8Array} systemId
  83. * @param {!Set<string>} keyIds
  84. * @param {number} version
  85. * @return {!Uint8Array}
  86. */
  87. static createPssh(data, systemId, keyIds, version) {
  88. goog.asserts.assert(systemId.byteLength == 16, 'Invalid system ID length');
  89. const dataLength = data.length;
  90. let psshSize = 0x4 + 0x4 + 0x4 + systemId.length + 0x4 + dataLength;
  91. if (version > 0) {
  92. psshSize += 0x4 + (16 * keyIds.size);
  93. }
  94. /** @type {!Uint8Array} */
  95. const psshBox = new Uint8Array(psshSize);
  96. /** @type {!DataView} */
  97. const psshData = shaka.util.BufferUtils.toDataView(psshBox);
  98. let byteCursor = 0;
  99. psshData.setUint32(byteCursor, psshSize);
  100. byteCursor += 0x4;
  101. psshData.setUint32(byteCursor, 0x70737368); // 'pssh'
  102. byteCursor += 0x4;
  103. (version < 1) ? psshData.setUint32(byteCursor, 0) :
  104. psshData.setUint32(byteCursor, 0x01000000); // version + flags
  105. byteCursor += 0x4;
  106. psshBox.set(systemId, byteCursor);
  107. byteCursor += systemId.length;
  108. // if version > 0, add KID count and kid values.
  109. if (version > 0) {
  110. psshData.setUint32(byteCursor, keyIds.size); // KID_count
  111. byteCursor += 0x4;
  112. const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
  113. for (const keyId of keyIds) {
  114. const KID = Uint8ArrayUtils.fromHex(keyId);
  115. psshBox.set(KID, byteCursor);
  116. byteCursor += KID.length;
  117. }
  118. }
  119. psshData.setUint32(byteCursor, dataLength);
  120. byteCursor += 0x4;
  121. psshBox.set(data, byteCursor);
  122. byteCursor += dataLength;
  123. goog.asserts.assert(byteCursor === psshSize, 'PSSH invalid length.');
  124. return psshBox;
  125. }
  126. /**
  127. * Returns just the data portion of a single PSSH
  128. *
  129. * @param {!Uint8Array} pssh
  130. * @return {!Uint8Array}
  131. */
  132. static getPsshData(pssh) {
  133. let offset = 8; // Box size and type fields
  134. /** @type {!DataView} */
  135. const view = shaka.util.BufferUtils.toDataView(pssh);
  136. // Read version
  137. const version = view.getUint8(offset);
  138. // Version (1), flags (3), system ID (16)
  139. offset += 20;
  140. if (version > 0) {
  141. // Key ID count (4) and All key IDs (16*count)
  142. offset += 4 + (16 * view.getUint32(offset));
  143. }
  144. // Data size
  145. offset += 4;
  146. return shaka.util.BufferUtils.toUint8(view, offset);
  147. }
  148. /**
  149. * Normalise the initData array. This is to apply browser specific
  150. * workarounds, e.g. removing duplicates which appears to occur
  151. * intermittently when the native msneedkey event fires (i.e. event.initData
  152. * contains dupes).
  153. *
  154. * @param {!Uint8Array} initData
  155. * @return {!Uint8Array}
  156. */
  157. static normaliseInitData(initData) {
  158. if (!initData) {
  159. return initData;
  160. }
  161. const pssh = new shaka.util.Pssh(initData);
  162. // If there is only a single pssh, return the original array.
  163. if (pssh.data.length <= 1) {
  164. return initData;
  165. }
  166. // Dedupe psshData.
  167. /** @type {!Array<!Uint8Array>} */
  168. const dedupedInitDatas = [];
  169. for (const initData of pssh.data) {
  170. const found = dedupedInitDatas.some((x) => {
  171. return shaka.util.BufferUtils.equal(x, initData);
  172. });
  173. if (!found) {
  174. dedupedInitDatas.push(initData);
  175. }
  176. }
  177. return shaka.util.Uint8ArrayUtils.concat(...dedupedInitDatas);
  178. }
  179. };