UNPKG

shaka-player

Version:
425 lines (364 loc) 13.6 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.media.ContentWorkarounds'); goog.require('goog.asserts'); goog.require('shaka.log'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.Lazy'); goog.require('shaka.util.Mp4Parser'); goog.require('shaka.util.Platform'); goog.require('shaka.util.Uint8ArrayUtils'); /** * @summary * A collection of methods to work around content issues on various platforms. */ shaka.media.ContentWorkarounds = class { /** * Transform the init segment into a new init segment buffer that indicates * encryption. If the init segment already indicates encryption, return the * original init segment. * * Should only be called for MP4 init segments, and only on platforms that * need this workaround. * * @param {!BufferSource} initSegmentBuffer * @return {!Uint8Array} * @see https://github.com/shaka-project/shaka-player/issues/2759 */ static fakeEncryption(initSegmentBuffer) { const ContentWorkarounds = shaka.media.ContentWorkarounds; let initSegment = shaka.util.BufferUtils.toUint8(initSegmentBuffer); let isEncrypted = false; /** @type {shaka.extern.ParsedBox} */ let stsdBox; const ancestorBoxes = []; const onSimpleAncestorBox = (box) => { ancestorBoxes.push(box); shaka.util.Mp4Parser.children(box); }; const onEncryptionMetadataBox = (box) => { isEncrypted = true; }; // Multiplexed content could have multiple boxes that we need to modify. // Add to this array in order of box offset. This will be important later, // when we process the boxes. /** @type {!Array.<{box: shaka.extern.ParsedBox, newType: number}>} */ const boxesToModify = []; new shaka.util.Mp4Parser() .box('moov', onSimpleAncestorBox) .box('trak', onSimpleAncestorBox) .box('mdia', onSimpleAncestorBox) .box('minf', onSimpleAncestorBox) .box('stbl', onSimpleAncestorBox) .fullBox('stsd', (box) => { stsdBox = box; ancestorBoxes.push(box); shaka.util.Mp4Parser.sampleDescription(box); }) .fullBox('encv', onEncryptionMetadataBox) .fullBox('enca', onEncryptionMetadataBox) .fullBox('hev1', (box) => { boxesToModify.push({ box, newType: ContentWorkarounds.BOX_TYPE_ENCV_, }); }) .fullBox('hvc1', (box) => { boxesToModify.push({ box, newType: ContentWorkarounds.BOX_TYPE_ENCV_, }); }) .fullBox('avc1', (box) => { boxesToModify.push({ box, newType: ContentWorkarounds.BOX_TYPE_ENCV_, }); }) .fullBox('avc3', (box) => { boxesToModify.push({ box, newType: ContentWorkarounds.BOX_TYPE_ENCV_, }); }) .fullBox('ac-3', (box) => { boxesToModify.push({ box, newType: ContentWorkarounds.BOX_TYPE_ENCA_, }); }) .fullBox('ec-3', (box) => { boxesToModify.push({ box, newType: ContentWorkarounds.BOX_TYPE_ENCA_, }); }) .fullBox('mp4a', (box) => { boxesToModify.push({ box, newType: ContentWorkarounds.BOX_TYPE_ENCA_, }); }).parse(initSegment); if (isEncrypted) { shaka.log.debug('Init segment already indicates encryption.'); return initSegment; } if (boxesToModify.length == 0 || !stsdBox) { shaka.log.error('Failed to find boxes needed to fake encryption!'); shaka.log.v2('Failed init segment (hex):', shaka.util.Uint8ArrayUtils.toHex(initSegment)); throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MEDIA, shaka.util.Error.Code.CONTENT_TRANSFORMATION_FAILED); } // Modify boxes in order from largest offset to smallest, so that earlier // boxes don't have their offsets changed before we process them. boxesToModify.reverse(); // in place! for (const workItem of boxesToModify) { const insertedBoxType = shaka.util.Mp4Parser.typeToString(workItem.newType); shaka.log.debug(`Inserting "${insertedBoxType}" box into init segment.`); initSegment = ContentWorkarounds.insertEncryptionMetadata_( initSegment, stsdBox, workItem.box, ancestorBoxes, workItem.newType); } return initSegment; } /** * Insert an encryption metadata box ("encv" or "enca" box) into the MP4 init * segment, based on the source box ("mp4a", "avc1", etc). Returns a new * buffer containing the modified init segment. * * @param {!Uint8Array} initSegment * @param {shaka.extern.ParsedBox} stsdBox * @param {shaka.extern.ParsedBox} sourceBox * @param {!Array.<shaka.extern.ParsedBox>} ancestorBoxes * @param {number} metadataBoxType * @return {!Uint8Array} * @private */ static insertEncryptionMetadata_( initSegment, stsdBox, sourceBox, ancestorBoxes, metadataBoxType) { const ContentWorkarounds = shaka.media.ContentWorkarounds; const metadataBoxArray = ContentWorkarounds.createEncryptionMetadata_( initSegment, sourceBox, metadataBoxType); // Construct a new init segment array with room for the encryption metadata // box we're adding. const newInitSegment = new Uint8Array(initSegment.byteLength + metadataBoxArray.byteLength); // For Xbox One, we cut and insert at the start of the source box. For // other platforms, we cut and insert at the end of the source box. It's // not clear why this is necessary on Xbox One, but it seems to be evidence // of another bug in the firmware implementation of MediaSource & EME. const cutPoint = shaka.util.Platform.isXboxOne() ? sourceBox.start : sourceBox.start + sourceBox.size; // The data before the cut point will be copied to the same location as // before. The data after that will be appended after the added metadata // box. const beforeData = initSegment.subarray(0, cutPoint); const afterData = initSegment.subarray(cutPoint); newInitSegment.set(beforeData); newInitSegment.set(metadataBoxArray, cutPoint); newInitSegment.set(afterData, cutPoint + metadataBoxArray.byteLength); // The parents up the chain from the encryption metadata box need their // sizes adjusted to account for the added box. These offsets should not be // changed, because they should all be within the first section we copy. for (const box of ancestorBoxes) { goog.asserts.assert(box.start < cutPoint, 'Ancestor MP4 box found in the wrong location! ' + 'Modified init segment will not make sense!'); ContentWorkarounds.updateBoxSize_( newInitSegment, box.start, box.size + metadataBoxArray.byteLength); } // Add one to the sample entries field of the "stsd" box. This is a 4-byte // field just past the box header. const stsdBoxView = shaka.util.BufferUtils.toDataView( newInitSegment, stsdBox.start); const stsdBoxHeaderSize = shaka.util.Mp4Parser.headerSize(stsdBox); const numEntries = stsdBoxView.getUint32(stsdBoxHeaderSize); stsdBoxView.setUint32(stsdBoxHeaderSize, numEntries + 1); return newInitSegment; } /** * Create an encryption metadata box ("encv" or "enca" box), based on the * source box ("mp4a", "avc1", etc). Returns a new buffer containing the * encryption metadata box. * * @param {!Uint8Array} initSegment * @param {shaka.extern.ParsedBox} sourceBox * @param {number} metadataBoxType * @return {!Uint8Array} * @private */ static createEncryptionMetadata_(initSegment, sourceBox, metadataBoxType) { const ContentWorkarounds = shaka.media.ContentWorkarounds; const sinfBoxArray = ContentWorkarounds.CANNED_SINF_BOX_.value(); // Create a subarray which points to the source box data. const sourceBoxArray = initSegment.subarray( /* start= */ sourceBox.start, /* end= */ sourceBox.start + sourceBox.size); // Create a view on the source box array. const sourceBoxView = shaka.util.BufferUtils.toDataView(sourceBoxArray); // Create an array to hold the new encryption metadata box, which is based // on the source box. const metadataBoxArray = new Uint8Array( sourceBox.size + sinfBoxArray.byteLength); // Copy the source box into the new array. metadataBoxArray.set(sourceBoxArray, /* targetOffset= */ 0); // Change the box type. const metadataBoxView = shaka.util.BufferUtils.toDataView(metadataBoxArray); metadataBoxView.setUint32( ContentWorkarounds.BOX_TYPE_OFFSET_, metadataBoxType); // Append the "sinf" box to the encryption metadata box. metadataBoxArray.set(sinfBoxArray, /* targetOffset= */ sourceBox.size); // Update the "sinf" box's format field (in the child "frma" box) to reflect // the format of the original source box. const sourceBoxType = sourceBoxView.getUint32( ContentWorkarounds.BOX_TYPE_OFFSET_); metadataBoxView.setUint32( sourceBox.size + ContentWorkarounds.CANNED_SINF_BOX_FORMAT_OFFSET_, sourceBoxType); // Now update the encryption metadata box size. ContentWorkarounds.updateBoxSize_( metadataBoxArray, /* boxStart= */ 0, metadataBoxArray.byteLength); return metadataBoxArray; } /** * Modify an MP4 box's size field in-place. * * @param {!Uint8Array} dataArray * @param {number} boxStart The start position of the box in dataArray. * @param {number} newBoxSize The new size of the box. * @private */ static updateBoxSize_(dataArray, boxStart, newBoxSize) { const ContentWorkarounds = shaka.media.ContentWorkarounds; const boxView = shaka.util.BufferUtils.toDataView(dataArray, boxStart); const sizeField = boxView.getUint32(ContentWorkarounds.BOX_SIZE_OFFSET_); if (sizeField == 0) { // Means "the rest of the box". // No adjustment needed for this box. } else if (sizeField == 1) { // Means "use 64-bit size box". // Set the 64-bit int in two 32-bit parts. // The high bits should definitely be 0 in practice, but we're being // thorough here. boxView.setUint32(ContentWorkarounds.BOX_SIZE_64_OFFSET_, newBoxSize >> 32); boxView.setUint32(ContentWorkarounds.BOX_SIZE_64_OFFSET_ + 4, newBoxSize & 0xffffffff); } else { // Normal 32-bit size field. // Not checking the size of the value here, since a box larger than 4GB is // unrealistic. boxView.setUint32(ContentWorkarounds.BOX_SIZE_OFFSET_, newBoxSize); } } }; /** * A canned "sinf" box for use when adding fake encryption metadata to init * segments. * * @const {!shaka.util.Lazy.<!Uint8Array>} * @private * @see https://github.com/shaka-project/shaka-player/issues/2759 */ shaka.media.ContentWorkarounds.CANNED_SINF_BOX_ = new shaka.util.Lazy(() => new Uint8Array([ // sinf box // Size: 0x50 = 80 0x00, 0x00, 0x00, 0x50, // Type: sinf 0x73, 0x69, 0x6e, 0x66, // Children of sinf... // frma box // Size: 0x0c = 12 0x00, 0x00, 0x00, 0x0c, // Type: frma (child of sinf) 0x66, 0x72, 0x6d, 0x61, // Format: filled in later based on the source box ("avc1", "mp4a", etc) 0x00, 0x00, 0x00, 0x00, // end of frma box // schm box // Size: 0x14 = 20 0x00, 0x00, 0x00, 0x14, // Type: schm (child of sinf) 0x73, 0x63, 0x68, 0x6d, // Version: 0, Flags: 0 0x00, 0x00, 0x00, 0x00, // Scheme: cenc 0x63, 0x65, 0x6e, 0x63, // Scheme version: 1.0 0x00, 0x01, 0x00, 0x00, // end of schm box // schi box // Size: 0x28 = 40 0x00, 0x00, 0x00, 0x28, // Type: schi (child of sinf) 0x73, 0x63, 0x68, 0x69, // Children of schi... // tenc box // Size: 0x20 = 32 0x00, 0x00, 0x00, 0x20, // Type: tenc (child of schi) 0x74, 0x65, 0x6e, 0x63, // Version: 0, Flags: 0 0x00, 0x00, 0x00, 0x00, // Reserved fields 0x00, 0x00, // Default protected: true 0x01, // Default per-sample IV size: 8 0x08, // Default key ID: all zeros (dummy) 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // end of tenc box // end of schi box // end of sinf box ])); /** * The location of the format field in the "frma" box inside the canned "sinf" * box above. * * @const {number} * @private */ shaka.media.ContentWorkarounds.CANNED_SINF_BOX_FORMAT_OFFSET_ = 0x10; /** * Offset to a box's size field. * * @const {number} * @private */ shaka.media.ContentWorkarounds.BOX_SIZE_OFFSET_ = 0; /** * Offset to a box's type field. * * @const {number} * @private */ shaka.media.ContentWorkarounds.BOX_TYPE_OFFSET_ = 4; /** * Offset to a box's 64-bit size field, if it has one. * * @const {number} * @private */ shaka.media.ContentWorkarounds.BOX_SIZE_64_OFFSET_ = 8; /** * Box type for "encv". * * @const {number} * @private */ shaka.media.ContentWorkarounds.BOX_TYPE_ENCV_ = 0x656e6376; /** * Box type for "enca". * * @const {number} * @private */ shaka.media.ContentWorkarounds.BOX_TYPE_ENCA_ = 0x656e6361;