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.Mp4BoxParsers');
  12. goog.require('shaka.util.Mp4Generator');
  13. goog.require('shaka.util.Mp4Parser');
  14. goog.require('shaka.util.Platform');
  15. goog.require('shaka.util.Uint8ArrayUtils');
  16. /**
  17. * @summary
  18. * A collection of methods to work around content issues on various platforms.
  19. */
  20. shaka.media.ContentWorkarounds = class {
  21. /**
  22. * For EAC-3 audio, the "enca" box ChannelCount field
  23. * MUST always be set to 2.
  24. * See ETSI TS 102 366 V1.2.1 Sec F.3
  25. *
  26. * Clients SHOULD ignore this value; however, it has been discovered that
  27. * some user agents don't, and instead, invalid values here can cause decoder
  28. * failures to be thrown (this has been observed with Chromecast).
  29. *
  30. * Given that this value MUST be hard-coded to 2, and that clients "SHALL"
  31. * ignore the value, it seems safe to manipulate the value to be 2 even when
  32. * the packager has provided a different value.
  33. * @param {!BufferSource} initSegmentBuffer
  34. * @return {!Uint8Array}
  35. */
  36. static correctEnca(initSegmentBuffer) {
  37. const initSegment = shaka.util.BufferUtils.toUint8(initSegmentBuffer);
  38. const modifiedInitSegment = initSegment;
  39. /**
  40. * Skip reserved bytes: 6
  41. * Skip data_reference_index: 2
  42. * Skip reserved bytes: 8
  43. */
  44. const encaChannelCountOffset = 16;
  45. /** @type {?DataView} */
  46. let encaBoxDataView = null;
  47. new shaka.util.Mp4Parser()
  48. .box('moov', shaka.util.Mp4Parser.children)
  49. .box('trak', shaka.util.Mp4Parser.children)
  50. .box('mdia', shaka.util.Mp4Parser.children)
  51. .box('minf', shaka.util.Mp4Parser.children)
  52. .box('stbl', shaka.util.Mp4Parser.children)
  53. .fullBox('stsd', shaka.util.Mp4Parser.sampleDescription)
  54. .box('enca', (box) => {
  55. encaBoxDataView = box.reader.getDataView();
  56. return shaka.util.Mp4Parser.audioSampleEntry(box);
  57. })
  58. .box('sinf', shaka.util.Mp4Parser.children)
  59. .box('frma', (box) => {
  60. box.parser.stop();
  61. const {codec} = shaka.util.Mp4BoxParsers.parseFRMA(box.reader);
  62. if (codec === 'ec-3' &&
  63. encaBoxDataView &&
  64. encaBoxDataView.getUint16(encaChannelCountOffset) !== 2
  65. ) {
  66. encaBoxDataView.setUint16(encaChannelCountOffset, 2);
  67. }
  68. })
  69. .parse(initSegment);
  70. return modifiedInitSegment;
  71. }
  72. /**
  73. * Transform the init segment into a new init segment buffer that indicates
  74. * encryption. If the init segment already indicates encryption, return the
  75. * original init segment.
  76. *
  77. * Should only be called for MP4 init segments, and only on platforms that
  78. * need this workaround.
  79. *
  80. * @param {!shaka.extern.Stream} stream
  81. * @param {!BufferSource} initSegmentBuffer
  82. * @param {?string} uri
  83. * @return {!Uint8Array}
  84. * @see https://github.com/shaka-project/shaka-player/issues/2759
  85. */
  86. static fakeEncryption(stream, initSegmentBuffer, uri) {
  87. const ContentWorkarounds = shaka.media.ContentWorkarounds;
  88. const initSegment = shaka.util.BufferUtils.toUint8(initSegmentBuffer);
  89. let modifiedInitSegment = initSegment;
  90. let isEncrypted = false;
  91. /** @type {shaka.extern.ParsedBox} */
  92. let stsdBox;
  93. const ancestorBoxes = [];
  94. const onSimpleAncestorBox = (box) => {
  95. ancestorBoxes.push(box);
  96. shaka.util.Mp4Parser.children(box);
  97. };
  98. const onEncryptionMetadataBox = (box) => {
  99. isEncrypted = true;
  100. box.parser.stop();
  101. };
  102. // Multiplexed content could have multiple boxes that we need to modify.
  103. // Add to this array in order of box offset. This will be important later,
  104. // when we process the boxes.
  105. /** @type {!Array<{box: shaka.extern.ParsedBox, newType: number}>} */
  106. const boxesToModify = [];
  107. const pushEncv = (box) => {
  108. boxesToModify.push({
  109. box,
  110. newType: ContentWorkarounds.BOX_TYPE_ENCV_,
  111. });
  112. };
  113. const pushEnca = (box) => {
  114. boxesToModify.push({
  115. box,
  116. newType: ContentWorkarounds.BOX_TYPE_ENCA_,
  117. });
  118. };
  119. new shaka.util.Mp4Parser()
  120. .box('moov', onSimpleAncestorBox)
  121. .box('trak', onSimpleAncestorBox)
  122. .box('mdia', onSimpleAncestorBox)
  123. .box('minf', onSimpleAncestorBox)
  124. .box('stbl', onSimpleAncestorBox)
  125. .fullBox('stsd', (box) => {
  126. stsdBox = box;
  127. ancestorBoxes.push(box);
  128. shaka.util.Mp4Parser.sampleDescription(box);
  129. })
  130. .fullBox('encv', onEncryptionMetadataBox)
  131. .fullBox('enca', onEncryptionMetadataBox)
  132. .fullBox('dvav', pushEncv)
  133. .fullBox('dva1', pushEncv)
  134. .fullBox('dvh1', pushEncv)
  135. .fullBox('dvhe', pushEncv)
  136. .fullBox('dvc1', pushEncv)
  137. .fullBox('dvi1', pushEncv)
  138. .fullBox('hev1', pushEncv)
  139. .fullBox('hvc1', pushEncv)
  140. .fullBox('avc1', pushEncv)
  141. .fullBox('avc3', pushEncv)
  142. .fullBox('ac-3', pushEnca)
  143. .fullBox('ec-3', pushEnca)
  144. .fullBox('ac-4', pushEnca)
  145. .fullBox('Opus', pushEnca)
  146. .fullBox('fLaC', pushEnca)
  147. .fullBox('mp4a', pushEnca)
  148. .parse(initSegment);
  149. if (isEncrypted) {
  150. shaka.log.debug('Init segment already indicates encryption.');
  151. return initSegment;
  152. }
  153. if (boxesToModify.length == 0 || !stsdBox) {
  154. shaka.log.error('Failed to find boxes needed to fake encryption!');
  155. shaka.log.v2('Failed init segment (hex):',
  156. shaka.util.Uint8ArrayUtils.toHex(initSegment));
  157. throw new shaka.util.Error(
  158. shaka.util.Error.Severity.CRITICAL,
  159. shaka.util.Error.Category.MEDIA,
  160. shaka.util.Error.Code.CONTENT_TRANSFORMATION_FAILED,
  161. uri);
  162. }
  163. // Modify boxes in order from largest offset to smallest, so that earlier
  164. // boxes don't have their offsets changed before we process them.
  165. boxesToModify.reverse(); // in place!
  166. for (const workItem of boxesToModify) {
  167. const insertedBoxType =
  168. shaka.util.Mp4Parser.typeToString(workItem.newType);
  169. shaka.log.debug(`Inserting "${insertedBoxType}" box into init segment.`);
  170. modifiedInitSegment = ContentWorkarounds.insertEncryptionMetadata_(
  171. stream, modifiedInitSegment, stsdBox, workItem.box, ancestorBoxes,
  172. workItem.newType);
  173. }
  174. // Edge Windows needs the unmodified init segment to be appended after the
  175. // patched one, otherwise video element throws following error:
  176. // CHUNK_DEMUXER_ERROR_APPEND_FAILED: Sample encryption info is not
  177. // available.
  178. if (shaka.util.Platform.isEdge() && shaka.util.Platform.isWindows() &&
  179. !shaka.util.Platform.isXboxOne()) {
  180. const doubleInitSegment = new Uint8Array(initSegment.byteLength +
  181. modifiedInitSegment.byteLength);
  182. doubleInitSegment.set(modifiedInitSegment);
  183. doubleInitSegment.set(initSegment, modifiedInitSegment.byteLength);
  184. return doubleInitSegment;
  185. }
  186. return modifiedInitSegment;
  187. }
  188. /**
  189. * @param {!BufferSource} mediaSegmentBuffer
  190. * @return {!Uint8Array}
  191. */
  192. static fakeMediaEncryption(mediaSegmentBuffer) {
  193. const mediaSegment = shaka.util.BufferUtils.toUint8(mediaSegmentBuffer);
  194. const mdatBoxes = [];
  195. new shaka.util.Mp4Parser()
  196. .box('mdat', (box) => {
  197. mdatBoxes.push(box);
  198. })
  199. .parse(mediaSegment);
  200. const newSegmentChunks = [];
  201. for (let i = 0; i < mdatBoxes.length; i++) {
  202. const prevMdat = mdatBoxes[i - 1];
  203. const currMdat = mdatBoxes[i];
  204. const chunkStart = prevMdat ? prevMdat.start + prevMdat.size : 0;
  205. const chunkEnd = currMdat.start + currMdat.size;
  206. const chunk = mediaSegment.subarray(chunkStart, chunkEnd);
  207. newSegmentChunks.push(
  208. shaka.media.ContentWorkarounds.fakeMediaEncryptionInChunk_(chunk));
  209. }
  210. return shaka.util.Uint8ArrayUtils.concat(...newSegmentChunks);
  211. }
  212. /**
  213. * @param {!Uint8Array} chunk
  214. * @return {!Uint8Array}
  215. * @private
  216. */
  217. static fakeMediaEncryptionInChunk_(chunk) {
  218. // Which track from stsd we want to use, 1-based.
  219. const desiredSampleDescriptionIndex = 2;
  220. let tfhdBox;
  221. let trunBox;
  222. let parsedTfhd;
  223. let parsedTrun;
  224. const ancestorBoxes = [];
  225. const onSimpleAncestorBox = (box) => {
  226. ancestorBoxes.push(box);
  227. shaka.util.Mp4Parser.children(box);
  228. };
  229. const onTfhdBox = (box) => {
  230. tfhdBox = box;
  231. parsedTfhd = shaka.util.Mp4BoxParsers.parseTFHD(box.reader, box.flags);
  232. };
  233. const onTrunBox = (box) => {
  234. trunBox = box;
  235. parsedTrun = shaka.util.Mp4BoxParsers.parseTRUN(box.reader, box.version,
  236. box.flags);
  237. };
  238. new shaka.util.Mp4Parser()
  239. .box('moof', onSimpleAncestorBox)
  240. .box('traf', onSimpleAncestorBox)
  241. .fullBox('tfhd', onTfhdBox)
  242. .fullBox('trun', onTrunBox)
  243. .parse(chunk);
  244. if (parsedTfhd && parsedTfhd.sampleDescriptionIndex !==
  245. desiredSampleDescriptionIndex) {
  246. const sdiPosition = tfhdBox.start +
  247. shaka.util.Mp4Parser.headerSize(tfhdBox) +
  248. 4 + // track_id
  249. (parsedTfhd.baseDataOffset !== null ? 8 : 0);
  250. const dataview = shaka.util.BufferUtils.toDataView(chunk);
  251. if (parsedTfhd.sampleDescriptionIndex !== null) {
  252. dataview.setUint32(sdiPosition, desiredSampleDescriptionIndex);
  253. } else {
  254. const sdiSize = 4; // uint32
  255. // first, update size & flags of tfhd
  256. shaka.media.ContentWorkarounds.updateBoxSize_(chunk,
  257. tfhdBox.start, tfhdBox.size + sdiSize);
  258. const versionAndFlags = dataview.getUint32(tfhdBox.start + 8);
  259. dataview.setUint32(tfhdBox.start + 8, versionAndFlags | 0x000002);
  260. // second, update trun
  261. if (parsedTrun && parsedTrun.dataOffset !== null) {
  262. const newDataOffset = parsedTrun.dataOffset + sdiSize;
  263. const dataOffsetPosition = trunBox.start +
  264. shaka.util.Mp4Parser.headerSize(trunBox) +
  265. 4; // sample count
  266. dataview.setInt32(dataOffsetPosition, newDataOffset);
  267. }
  268. const beforeSdi = chunk.subarray(0, sdiPosition);
  269. const afterSdi = chunk.subarray(sdiPosition);
  270. chunk = new Uint8Array(chunk.byteLength + sdiSize);
  271. chunk.set(beforeSdi);
  272. const bytes = [];
  273. for (let byte = sdiSize - 1; byte >= 0; byte--) {
  274. bytes.push((desiredSampleDescriptionIndex >> (8 * byte)) & 0xff);
  275. }
  276. chunk.set(new Uint8Array(bytes), sdiPosition);
  277. chunk.set(afterSdi, sdiPosition + sdiSize);
  278. for (const box of ancestorBoxes) {
  279. shaka.media.ContentWorkarounds.updateBoxSize_(chunk, box.start,
  280. box.size + sdiSize);
  281. }
  282. }
  283. }
  284. return chunk;
  285. }
  286. /**
  287. * Insert an encryption metadata box ("encv" or "enca" box) into the MP4 init
  288. * segment, based on the source box ("mp4a", "avc1", etc). Returns a new
  289. * buffer containing the modified init segment.
  290. *
  291. * @param {!shaka.extern.Stream} stream
  292. * @param {!Uint8Array} initSegment
  293. * @param {shaka.extern.ParsedBox} stsdBox
  294. * @param {shaka.extern.ParsedBox} sourceBox
  295. * @param {!Array<shaka.extern.ParsedBox>} ancestorBoxes
  296. * @param {number} metadataBoxType
  297. * @return {!Uint8Array}
  298. * @private
  299. */
  300. static insertEncryptionMetadata_(
  301. stream, initSegment, stsdBox, sourceBox, ancestorBoxes, metadataBoxType) {
  302. const ContentWorkarounds = shaka.media.ContentWorkarounds;
  303. const metadataBoxArray = ContentWorkarounds.createEncryptionMetadata_(
  304. stream, initSegment, sourceBox, metadataBoxType);
  305. // Construct a new init segment array with room for the encryption metadata
  306. // box we're adding.
  307. const newInitSegment =
  308. new Uint8Array(initSegment.byteLength + metadataBoxArray.byteLength);
  309. // For Xbox One & Edge, we cut and insert at the start of the source box.
  310. // For other platforms, we cut and insert at the end of the source box. It's
  311. // not clear why this is necessary on Xbox One, but it seems to be evidence
  312. // of another bug in the firmware implementation of MediaSource & EME.
  313. const cutPoint = (shaka.util.Platform.isApple() ||
  314. shaka.util.Platform.isXboxOne() || shaka.util.Platform.isEdge()) ?
  315. sourceBox.start :
  316. sourceBox.start + sourceBox.size;
  317. // The data before the cut point will be copied to the same location as
  318. // before. The data after that will be appended after the added metadata
  319. // box.
  320. const beforeData = initSegment.subarray(0, cutPoint);
  321. const afterData = initSegment.subarray(cutPoint);
  322. newInitSegment.set(beforeData);
  323. newInitSegment.set(metadataBoxArray, cutPoint);
  324. newInitSegment.set(afterData, cutPoint + metadataBoxArray.byteLength);
  325. // The parents up the chain from the encryption metadata box need their
  326. // sizes adjusted to account for the added box. These offsets should not be
  327. // changed, because they should all be within the first section we copy.
  328. for (const box of ancestorBoxes) {
  329. goog.asserts.assert(box.start < cutPoint,
  330. 'Ancestor MP4 box found in the wrong location! ' +
  331. 'Modified init segment will not make sense!');
  332. ContentWorkarounds.updateBoxSize_(
  333. newInitSegment, box.start, box.size + metadataBoxArray.byteLength);
  334. }
  335. // Add one to the sample entries field of the "stsd" box. This is a 4-byte
  336. // field just past the box header.
  337. const stsdBoxView = shaka.util.BufferUtils.toDataView(
  338. newInitSegment, stsdBox.start);
  339. const stsdBoxHeaderSize = shaka.util.Mp4Parser.headerSize(stsdBox);
  340. const numEntries = stsdBoxView.getUint32(stsdBoxHeaderSize);
  341. stsdBoxView.setUint32(stsdBoxHeaderSize, numEntries + 1);
  342. return newInitSegment;
  343. }
  344. /**
  345. * Create an encryption metadata box ("encv" or "enca" box), based on the
  346. * source box ("mp4a", "avc1", etc). Returns a new buffer containing the
  347. * encryption metadata box.
  348. *
  349. * @param {!shaka.extern.Stream} stream
  350. * @param {!Uint8Array} initSegment
  351. * @param {shaka.extern.ParsedBox} sourceBox
  352. * @param {number} metadataBoxType
  353. * @return {!Uint8Array}
  354. * @private
  355. */
  356. static createEncryptionMetadata_(stream, initSegment, sourceBox,
  357. metadataBoxType) {
  358. const ContentWorkarounds = shaka.media.ContentWorkarounds;
  359. const mp4Generator = new shaka.util.Mp4Generator([]);
  360. const sinfBoxArray = mp4Generator.sinf(stream, sourceBox.name);
  361. // Create a subarray which points to the source box data.
  362. const sourceBoxArray = initSegment.subarray(
  363. /* start= */ sourceBox.start,
  364. /* end= */ sourceBox.start + sourceBox.size);
  365. // Create an array to hold the new encryption metadata box, which is based
  366. // on the source box.
  367. const metadataBoxArray = new Uint8Array(
  368. sourceBox.size + sinfBoxArray.byteLength);
  369. // Copy the source box into the new array.
  370. metadataBoxArray.set(sourceBoxArray, /* targetOffset= */ 0);
  371. // Change the box type.
  372. const metadataBoxView = shaka.util.BufferUtils.toDataView(metadataBoxArray);
  373. metadataBoxView.setUint32(
  374. ContentWorkarounds.BOX_TYPE_OFFSET_, metadataBoxType);
  375. // Append the "sinf" box to the encryption metadata box.
  376. metadataBoxArray.set(sinfBoxArray, /* targetOffset= */ sourceBox.size);
  377. // Now update the encryption metadata box size.
  378. ContentWorkarounds.updateBoxSize_(
  379. metadataBoxArray, /* boxStart= */ 0, metadataBoxArray.byteLength);
  380. return metadataBoxArray;
  381. }
  382. /**
  383. * Modify an MP4 box's size field in-place.
  384. *
  385. * @param {!Uint8Array} dataArray
  386. * @param {number} boxStart The start position of the box in dataArray.
  387. * @param {number} newBoxSize The new size of the box.
  388. * @private
  389. */
  390. static updateBoxSize_(dataArray, boxStart, newBoxSize) {
  391. const ContentWorkarounds = shaka.media.ContentWorkarounds;
  392. const boxView = shaka.util.BufferUtils.toDataView(dataArray, boxStart);
  393. const sizeField = boxView.getUint32(ContentWorkarounds.BOX_SIZE_OFFSET_);
  394. if (sizeField == 0) { // Means "the rest of the box".
  395. // No adjustment needed for this box.
  396. } else if (sizeField == 1) { // Means "use 64-bit size box".
  397. // Set the 64-bit int in two 32-bit parts.
  398. // The high bits should definitely be 0 in practice, but we're being
  399. // thorough here.
  400. boxView.setUint32(ContentWorkarounds.BOX_SIZE_64_OFFSET_,
  401. newBoxSize >> 32);
  402. boxView.setUint32(ContentWorkarounds.BOX_SIZE_64_OFFSET_ + 4,
  403. newBoxSize & 0xffffffff);
  404. } else { // Normal 32-bit size field.
  405. // Not checking the size of the value here, since a box larger than 4GB is
  406. // unrealistic.
  407. boxView.setUint32(ContentWorkarounds.BOX_SIZE_OFFSET_, newBoxSize);
  408. }
  409. }
  410. /**
  411. * Transform the init segment into a new init segment buffer that indicates
  412. * EC-3 as audio codec instead of AC-3. Even though any EC-3 decoder should
  413. * be able to decode AC-3 streams, there are platforms that do not accept
  414. * AC-3 as codec.
  415. *
  416. * Should only be called for MP4 init segments, and only on platforms that
  417. * need this workaround. Returns a new buffer containing the modified init
  418. * segment.
  419. *
  420. * @param {!BufferSource} initSegmentBuffer
  421. * @return {!Uint8Array}
  422. */
  423. static fakeEC3(initSegmentBuffer) {
  424. const ContentWorkarounds = shaka.media.ContentWorkarounds;
  425. const initSegment = shaka.util.BufferUtils.toUint8(initSegmentBuffer);
  426. const ancestorBoxes = [];
  427. const onSimpleAncestorBox = (box) => {
  428. ancestorBoxes.push({start: box.start, size: box.size});
  429. shaka.util.Mp4Parser.children(box);
  430. };
  431. new shaka.util.Mp4Parser()
  432. .box('moov', onSimpleAncestorBox)
  433. .box('trak', onSimpleAncestorBox)
  434. .box('mdia', onSimpleAncestorBox)
  435. .box('minf', onSimpleAncestorBox)
  436. .box('stbl', onSimpleAncestorBox)
  437. .box('stsd', (box) => {
  438. ancestorBoxes.push({start: box.start, size: box.size});
  439. const stsdBoxView = shaka.util.BufferUtils.toDataView(
  440. initSegment, box.start);
  441. // "size - 3" is because we immediately read a uint32.
  442. for (let i = 0; i < box.size -3; i++) {
  443. const codecTag = stsdBoxView.getUint32(i);
  444. if (codecTag == ContentWorkarounds.BOX_TYPE_AC_3_) {
  445. stsdBoxView.setUint32(i, ContentWorkarounds.BOX_TYPE_EC_3_);
  446. } else if (codecTag == ContentWorkarounds.BOX_TYPE_DAC3_) {
  447. stsdBoxView.setUint32(i, ContentWorkarounds.BOX_TYPE_DEC3_);
  448. }
  449. }
  450. }).parse(initSegment);
  451. return initSegment;
  452. }
  453. };
  454. /**
  455. * Offset to a box's size field.
  456. *
  457. * @const {number}
  458. * @private
  459. */
  460. shaka.media.ContentWorkarounds.BOX_SIZE_OFFSET_ = 0;
  461. /**
  462. * Offset to a box's type field.
  463. *
  464. * @const {number}
  465. * @private
  466. */
  467. shaka.media.ContentWorkarounds.BOX_TYPE_OFFSET_ = 4;
  468. /**
  469. * Offset to a box's 64-bit size field, if it has one.
  470. *
  471. * @const {number}
  472. * @private
  473. */
  474. shaka.media.ContentWorkarounds.BOX_SIZE_64_OFFSET_ = 8;
  475. /**
  476. * Box type for "encv".
  477. *
  478. * @const {number}
  479. * @private
  480. */
  481. shaka.media.ContentWorkarounds.BOX_TYPE_ENCV_ = 0x656e6376;
  482. /**
  483. * Box type for "enca".
  484. *
  485. * @const {number}
  486. * @private
  487. */
  488. shaka.media.ContentWorkarounds.BOX_TYPE_ENCA_ = 0x656e6361;
  489. /**
  490. * Box type for "ac-3".
  491. *
  492. * @const {number}
  493. * @private
  494. */
  495. shaka.media.ContentWorkarounds.BOX_TYPE_AC_3_ = 0x61632d33;
  496. /**
  497. * Box type for "dac3".
  498. *
  499. * @const {number}
  500. * @private
  501. */
  502. shaka.media.ContentWorkarounds.BOX_TYPE_DAC3_ = 0x64616333;
  503. /**
  504. * Box type for "ec-3".
  505. *
  506. * @const {number}
  507. * @private
  508. */
  509. shaka.media.ContentWorkarounds.BOX_TYPE_EC_3_ = 0x65632d33;
  510. /**
  511. * Box type for "dec3".
  512. *
  513. * @const {number}
  514. * @private
  515. */
  516. shaka.media.ContentWorkarounds.BOX_TYPE_DEC3_ = 0x64656333;