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