mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-16 16:16:40 +03:00
d5b1863157
On Edge, to properly play mixed content, we need to insert init segment twice for clear part - once as encrypted, and immediately again as clear. Otherwise we may encounter green screen and errors from video decoder. Co-authored-by: Joey Parrish <joeyparrish@users.noreply.github.com> Co-authored-by: Nick Michael <nick-michael@users.noreply.github.com> Co-authored-by: Álvaro Velad Galván <ladvan91@hotmail.com> Co-authored-by: Casey Occhialini <1508707+littlespex@users.noreply.github.com>
472 lines
15 KiB
JavaScript
472 lines
15 KiB
JavaScript
/*! @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
|
|
* @param {?string} uri
|
|
* @return {!Uint8Array}
|
|
* @see https://github.com/shaka-project/shaka-player/issues/2759
|
|
*/
|
|
static fakeEncryption(initSegmentBuffer, uri) {
|
|
const ContentWorkarounds = shaka.media.ContentWorkarounds;
|
|
const initSegment = shaka.util.BufferUtils.toUint8(initSegmentBuffer);
|
|
let modifiedInitSegment = initSegment;
|
|
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('dvav', (box) => {
|
|
boxesToModify.push({
|
|
box,
|
|
newType: ContentWorkarounds.BOX_TYPE_ENCV_,
|
|
});
|
|
})
|
|
.fullBox('dva1', (box) => {
|
|
boxesToModify.push({
|
|
box,
|
|
newType: ContentWorkarounds.BOX_TYPE_ENCV_,
|
|
});
|
|
})
|
|
.fullBox('dvh1', (box) => {
|
|
boxesToModify.push({
|
|
box,
|
|
newType: ContentWorkarounds.BOX_TYPE_ENCV_,
|
|
});
|
|
})
|
|
.fullBox('dvhe', (box) => {
|
|
boxesToModify.push({
|
|
box,
|
|
newType: ContentWorkarounds.BOX_TYPE_ENCV_,
|
|
});
|
|
})
|
|
.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('ac-4', (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,
|
|
uri);
|
|
}
|
|
|
|
// 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.`);
|
|
modifiedInitSegment = ContentWorkarounds.insertEncryptionMetadata_(
|
|
modifiedInitSegment, stsdBox, workItem.box, ancestorBoxes,
|
|
workItem.newType);
|
|
}
|
|
|
|
// Edge Windows needs the unmodified init segment to be appended after the
|
|
// patched one, otherwise video element throws following error:
|
|
// CHUNK_DEMUXER_ERROR_APPEND_FAILED: Sample encryption info is not
|
|
// available.
|
|
if (shaka.util.Platform.isEdge() && shaka.util.Platform.isWindows()) {
|
|
const doubleInitSegment = new Uint8Array(initSegment.byteLength +
|
|
modifiedInitSegment.byteLength);
|
|
doubleInitSegment.set(modifiedInitSegment);
|
|
doubleInitSegment.set(initSegment, modifiedInitSegment.byteLength);
|
|
return doubleInitSegment;
|
|
}
|
|
|
|
return modifiedInitSegment;
|
|
}
|
|
|
|
/**
|
|
* 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 & Edge, 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() || shaka.util.Platform.isEdge()) ?
|
|
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;
|