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