Source: lib/media/content_workarounds.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.ContentWorkarounds');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.util.BufferUtils');
  10. goog.require('shaka.util.Error');
  11. goog.require('shaka.util.Lazy');
  12. goog.require('shaka.util.Mp4Parser');
  13. goog.require('shaka.util.Platform');
  14. goog.require('shaka.util.Uint8ArrayUtils');
  15. /**
  16. * @summary
  17. * A collection of methods to work around content issues on various platforms.
  18. */
  19. shaka.media.ContentWorkarounds = class {
  20. /**
  21. * Transform the init segment into a new init segment buffer that indicates
  22. * encryption. If the init segment already indicates encryption, return the
  23. * original init segment.
  24. *
  25. * Should only be called for MP4 init segments, and only on platforms that
  26. * need this workaround.
  27. *
  28. * @param {!BufferSource} initSegmentBuffer
  29. * @return {!Uint8Array}
  30. * @see https://github.com/shaka-project/shaka-player/issues/2759
  31. */
  32. static fakeEncryption(initSegmentBuffer) {
  33. const ContentWorkarounds = shaka.media.ContentWorkarounds;
  34. let initSegment = shaka.util.BufferUtils.toUint8(initSegmentBuffer);
  35. let isEncrypted = false;
  36. /** @type {shaka.extern.ParsedBox} */
  37. let stsdBox;
  38. const ancestorBoxes = [];
  39. const onSimpleAncestorBox = (box) => {
  40. ancestorBoxes.push(box);
  41. shaka.util.Mp4Parser.children(box);
  42. };
  43. const onEncryptionMetadataBox = (box) => {
  44. isEncrypted = true;
  45. };
  46. // Multiplexed content could have multiple boxes that we need to modify.
  47. // Add to this array in order of box offset. This will be important later,
  48. // when we process the boxes.
  49. /** @type {!Array.<{box: shaka.extern.ParsedBox, newType: number}>} */
  50. const boxesToModify = [];
  51. new shaka.util.Mp4Parser()
  52. .box('moov', onSimpleAncestorBox)
  53. .box('trak', onSimpleAncestorBox)
  54. .box('mdia', onSimpleAncestorBox)
  55. .box('minf', onSimpleAncestorBox)
  56. .box('stbl', onSimpleAncestorBox)
  57. .fullBox('stsd', (box) => {
  58. stsdBox = box;
  59. ancestorBoxes.push(box);
  60. shaka.util.Mp4Parser.sampleDescription(box);
  61. })
  62. .fullBox('encv', onEncryptionMetadataBox)
  63. .fullBox('enca', onEncryptionMetadataBox)
  64. .fullBox('avc1', (box) => {
  65. boxesToModify.push({
  66. box,
  67. newType: ContentWorkarounds.BOX_TYPE_ENCV_,
  68. });
  69. })
  70. .fullBox('avc3', (box) => {
  71. boxesToModify.push({
  72. box,
  73. newType: ContentWorkarounds.BOX_TYPE_ENCV_,
  74. });
  75. })
  76. .fullBox('ac-3', (box) => {
  77. boxesToModify.push({
  78. box,
  79. newType: ContentWorkarounds.BOX_TYPE_ENCA_,
  80. });
  81. })
  82. .fullBox('ec-3', (box) => {
  83. boxesToModify.push({
  84. box,
  85. newType: ContentWorkarounds.BOX_TYPE_ENCA_,
  86. });
  87. })
  88. .fullBox('mp4a', (box) => {
  89. boxesToModify.push({
  90. box,
  91. newType: ContentWorkarounds.BOX_TYPE_ENCA_,
  92. });
  93. }).parse(initSegment);
  94. if (isEncrypted) {
  95. shaka.log.debug('Init segment already indicates encryption.');
  96. return initSegment;
  97. }
  98. if (boxesToModify.length == 0 || !stsdBox) {
  99. shaka.log.error('Failed to find boxes needed to fake encryption!');
  100. shaka.log.v2('Failed init segment (hex):',
  101. shaka.util.Uint8ArrayUtils.toHex(initSegment));
  102. throw new shaka.util.Error(
  103. shaka.util.Error.Severity.CRITICAL,
  104. shaka.util.Error.Category.MEDIA,
  105. shaka.util.Error.Code.CONTENT_TRANSFORMATION_FAILED);
  106. }
  107. // Modify boxes in order from largest offset to smallest, so that earlier
  108. // boxes don't have their offsets changed before we process them.
  109. boxesToModify.reverse(); // in place!
  110. for (const workItem of boxesToModify) {
  111. const insertedBoxType =
  112. shaka.util.Mp4Parser.typeToString(workItem.newType);
  113. shaka.log.debug(`Inserting "${insertedBoxType}" box into init segment.`);
  114. initSegment = ContentWorkarounds.insertEncryptionMetadata_(
  115. initSegment, stsdBox, workItem.box, ancestorBoxes, workItem.newType);
  116. }
  117. return initSegment;
  118. }
  119. /**
  120. * Insert an encryption metadata box ("encv" or "enca" box) into the MP4 init
  121. * segment, based on the source box ("mp4a", "avc1", etc). Returns a new
  122. * buffer containing the modified init segment.
  123. *
  124. * @param {!Uint8Array} initSegment
  125. * @param {shaka.extern.ParsedBox} stsdBox
  126. * @param {shaka.extern.ParsedBox} sourceBox
  127. * @param {!Array.<shaka.extern.ParsedBox>} ancestorBoxes
  128. * @param {number} metadataBoxType
  129. * @return {!Uint8Array}
  130. * @private
  131. */
  132. static insertEncryptionMetadata_(
  133. initSegment, stsdBox, sourceBox, ancestorBoxes, metadataBoxType) {
  134. const ContentWorkarounds = shaka.media.ContentWorkarounds;
  135. const metadataBoxArray = ContentWorkarounds.createEncryptionMetadata_(
  136. initSegment, sourceBox, metadataBoxType);
  137. // Construct a new init segment array with room for the encryption metadata
  138. // box we're adding.
  139. const newInitSegment =
  140. new Uint8Array(initSegment.byteLength + metadataBoxArray.byteLength);
  141. // For Xbox One, we cut and insert at the start of the source box. For
  142. // other platforms, we cut and insert at the end of the source box. It's
  143. // not clear why this is necessary on Xbox One, but it seems to be evidence
  144. // of another bug in the firmware implementation of MediaSource & EME.
  145. const cutPoint = shaka.util.Platform.isXboxOne() ?
  146. sourceBox.start :
  147. sourceBox.start + sourceBox.size;
  148. // The data before the cut point will be copied to the same location as
  149. // before. The data after that will be appended after the added metadata
  150. // box.
  151. const beforeData = initSegment.subarray(0, cutPoint);
  152. const afterData = initSegment.subarray(cutPoint);
  153. newInitSegment.set(beforeData);
  154. newInitSegment.set(metadataBoxArray, cutPoint);
  155. newInitSegment.set(afterData, cutPoint + metadataBoxArray.byteLength);
  156. // The parents up the chain from the encryption metadata box need their
  157. // sizes adjusted to account for the added box. These offsets should not be
  158. // changed, because they should all be within the first section we copy.
  159. for (const box of ancestorBoxes) {
  160. goog.asserts.assert(box.start < cutPoint,
  161. 'Ancestor MP4 box found in the wrong location! ' +
  162. 'Modified init segment will not make sense!');
  163. ContentWorkarounds.updateBoxSize_(
  164. newInitSegment, box.start, box.size + metadataBoxArray.byteLength);
  165. }
  166. // Add one to the sample entries field of the "stsd" box. This is a 4-byte
  167. // field just past the box header.
  168. const stsdBoxView = shaka.util.BufferUtils.toDataView(
  169. newInitSegment, stsdBox.start);
  170. const stsdBoxHeaderSize = shaka.util.Mp4Parser.headerSize(stsdBox);
  171. const numEntries = stsdBoxView.getUint32(stsdBoxHeaderSize);
  172. stsdBoxView.setUint32(stsdBoxHeaderSize, numEntries + 1);
  173. return newInitSegment;
  174. }
  175. /**
  176. * Create an encryption metadata box ("encv" or "enca" box), based on the
  177. * source box ("mp4a", "avc1", etc). Returns a new buffer containing the
  178. * encryption metadata box.
  179. *
  180. * @param {!Uint8Array} initSegment
  181. * @param {shaka.extern.ParsedBox} sourceBox
  182. * @param {number} metadataBoxType
  183. * @return {!Uint8Array}
  184. * @private
  185. */
  186. static createEncryptionMetadata_(initSegment, sourceBox, metadataBoxType) {
  187. const ContentWorkarounds = shaka.media.ContentWorkarounds;
  188. const sinfBoxArray = ContentWorkarounds.CANNED_SINF_BOX_.value();
  189. // Create a subarray which points to the source box data.
  190. const sourceBoxArray = initSegment.subarray(
  191. /* start= */ sourceBox.start,
  192. /* end= */ sourceBox.start + sourceBox.size);
  193. // Create a view on the source box array.
  194. const sourceBoxView = shaka.util.BufferUtils.toDataView(sourceBoxArray);
  195. // Create an array to hold the new encryption metadata box, which is based
  196. // on the source box.
  197. const metadataBoxArray = new Uint8Array(
  198. sourceBox.size + sinfBoxArray.byteLength);
  199. // Copy the source box into the new array.
  200. metadataBoxArray.set(sourceBoxArray, /* targetOffset= */ 0);
  201. // Change the box type.
  202. const metadataBoxView = shaka.util.BufferUtils.toDataView(metadataBoxArray);
  203. metadataBoxView.setUint32(
  204. ContentWorkarounds.BOX_TYPE_OFFSET_, metadataBoxType);
  205. // Append the "sinf" box to the encryption metadata box.
  206. metadataBoxArray.set(sinfBoxArray, /* targetOffset= */ sourceBox.size);
  207. // Update the "sinf" box's format field (in the child "frma" box) to reflect
  208. // the format of the original source box.
  209. const sourceBoxType = sourceBoxView.getUint32(
  210. ContentWorkarounds.BOX_TYPE_OFFSET_);
  211. metadataBoxView.setUint32(
  212. sourceBox.size + ContentWorkarounds.CANNED_SINF_BOX_FORMAT_OFFSET_,
  213. sourceBoxType);
  214. // Now update the encryption metadata box size.
  215. ContentWorkarounds.updateBoxSize_(
  216. metadataBoxArray, /* boxStart= */ 0, metadataBoxArray.byteLength);
  217. return metadataBoxArray;
  218. }
  219. /**
  220. * Modify an MP4 box's size field in-place.
  221. *
  222. * @param {!Uint8Array} dataArray
  223. * @param {number} boxStart The start position of the box in dataArray.
  224. * @param {number} newBoxSize The new size of the box.
  225. * @private
  226. */
  227. static updateBoxSize_(dataArray, boxStart, newBoxSize) {
  228. const ContentWorkarounds = shaka.media.ContentWorkarounds;
  229. const boxView = shaka.util.BufferUtils.toDataView(dataArray, boxStart);
  230. const sizeField = boxView.getUint32(ContentWorkarounds.BOX_SIZE_OFFSET_);
  231. if (sizeField == 0) { // Means "the rest of the box".
  232. // No adjustment needed for this box.
  233. } else if (sizeField == 1) { // Means "use 64-bit size box".
  234. // Set the 64-bit int in two 32-bit parts.
  235. // The high bits should definitely be 0 in practice, but we're being
  236. // thorough here.
  237. boxView.setUint32(ContentWorkarounds.BOX_SIZE_64_OFFSET_,
  238. newBoxSize >> 32);
  239. boxView.setUint32(ContentWorkarounds.BOX_SIZE_64_OFFSET_ + 4,
  240. newBoxSize & 0xffffffff);
  241. } else { // Normal 32-bit size field.
  242. // Not checking the size of the value here, since a box larger than 4GB is
  243. // unrealistic.
  244. boxView.setUint32(ContentWorkarounds.BOX_SIZE_OFFSET_, newBoxSize);
  245. }
  246. }
  247. };
  248. /**
  249. * A canned "sinf" box for use when adding fake encryption metadata to init
  250. * segments.
  251. *
  252. * @const {!shaka.util.Lazy.<!Uint8Array>}
  253. * @private
  254. * @see https://github.com/shaka-project/shaka-player/issues/2759
  255. */
  256. shaka.media.ContentWorkarounds.CANNED_SINF_BOX_ =
  257. new shaka.util.Lazy(() => new Uint8Array([
  258. // sinf box
  259. // Size: 0x50 = 80
  260. 0x00, 0x00, 0x00, 0x50,
  261. // Type: sinf
  262. 0x73, 0x69, 0x6e, 0x66,
  263. // Children of sinf...
  264. // frma box
  265. // Size: 0x0c = 12
  266. 0x00, 0x00, 0x00, 0x0c,
  267. // Type: frma (child of sinf)
  268. 0x66, 0x72, 0x6d, 0x61,
  269. // Format: filled in later based on the source box ("avc1", "mp4a", etc)
  270. 0x00, 0x00, 0x00, 0x00,
  271. // end of frma box
  272. // schm box
  273. // Size: 0x14 = 20
  274. 0x00, 0x00, 0x00, 0x14,
  275. // Type: schm (child of sinf)
  276. 0x73, 0x63, 0x68, 0x6d,
  277. // Version: 0, Flags: 0
  278. 0x00, 0x00, 0x00, 0x00,
  279. // Scheme: cenc
  280. 0x63, 0x65, 0x6e, 0x63,
  281. // Scheme version: 1.0
  282. 0x00, 0x01, 0x00, 0x00,
  283. // end of schm box
  284. // schi box
  285. // Size: 0x28 = 40
  286. 0x00, 0x00, 0x00, 0x28,
  287. // Type: schi (child of sinf)
  288. 0x73, 0x63, 0x68, 0x69,
  289. // Children of schi...
  290. // tenc box
  291. // Size: 0x20 = 32
  292. 0x00, 0x00, 0x00, 0x20,
  293. // Type: tenc (child of schi)
  294. 0x74, 0x65, 0x6e, 0x63,
  295. // Version: 0, Flags: 0
  296. 0x00, 0x00, 0x00, 0x00,
  297. // Reserved fields
  298. 0x00, 0x00,
  299. // Default protected: true
  300. 0x01,
  301. // Default per-sample IV size: 8
  302. 0x08,
  303. // Default key ID: all zeros (dummy)
  304. 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  305. 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  306. // end of tenc box
  307. // end of schi box
  308. // end of sinf box
  309. ]));
  310. /**
  311. * The location of the format field in the "frma" box inside the canned "sinf"
  312. * box above.
  313. *
  314. * @const {number}
  315. * @private
  316. */
  317. shaka.media.ContentWorkarounds.CANNED_SINF_BOX_FORMAT_OFFSET_ = 0x10;
  318. /**
  319. * Offset to a box's size field.
  320. *
  321. * @const {number}
  322. * @private
  323. */
  324. shaka.media.ContentWorkarounds.BOX_SIZE_OFFSET_ = 0;
  325. /**
  326. * Offset to a box's type field.
  327. *
  328. * @const {number}
  329. * @private
  330. */
  331. shaka.media.ContentWorkarounds.BOX_TYPE_OFFSET_ = 4;
  332. /**
  333. * Offset to a box's 64-bit size field, if it has one.
  334. *
  335. * @const {number}
  336. * @private
  337. */
  338. shaka.media.ContentWorkarounds.BOX_SIZE_64_OFFSET_ = 8;
  339. /**
  340. * Box type for "encv".
  341. *
  342. * @const {number}
  343. * @private
  344. */
  345. shaka.media.ContentWorkarounds.BOX_TYPE_ENCV_ = 0x656e6376;
  346. /**
  347. * Box type for "enca".
  348. *
  349. * @const {number}
  350. * @private
  351. */
  352. shaka.media.ContentWorkarounds.BOX_TYPE_ENCA_ = 0x656e6361;