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