UNPKG

shaka-player

Version:
395 lines (358 loc) 13.3 kB
/*! @license * MSS Transmuxer * Copyright 2015 Dash Industry Forum * SPDX-License-Identifier: BSD-3-Clause */ /* * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - Neither the name of the Dash Industry Forum nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ // cspell:words crypted usertype goog.provide('shaka.transmuxer.MssTransmuxer'); goog.require('shaka.media.Capabilities'); goog.require('shaka.transmuxer.TransmuxerEngine'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.dependencies'); goog.requireType('shaka.media.SegmentReference'); /** * @implements {shaka.extern.Transmuxer} * @export */ shaka.transmuxer.MssTransmuxer = class { /** * @param {string} mimeType */ constructor(mimeType) { /** @private {string} */ this.originalMimeType_ = mimeType; /** @private {?ISOBoxer} */ this.isoBoxer_ = shaka.dependencies.isoBoxer(); if (this.isoBoxer_) { this.addSpecificBoxProcessor_(); } } /** * Add specific box processor for codem-isoboxer * * @private */ addSpecificBoxProcessor_() { // eslint-disable-next-line no-restricted-syntax this.isoBoxer_.addBoxProcessor('saio', function() { // eslint-disable-next-line no-invalid-this const box = /** @type {!ISOBox} */(this); box._procFullBox(); if (box.flags & 1) { box._procField('aux_info_type', 'uint', 32); box._procField('aux_info_type_parameter', 'uint', 32); } box._procField('entry_count', 'uint', 32); box._procFieldArray('offset', box.entry_count, 'uint', (box.version === 1) ? 64 : 32); }); // eslint-disable-next-line no-restricted-syntax this.isoBoxer_.addBoxProcessor('saiz', function() { // eslint-disable-next-line no-invalid-this const box = /** @type {!ISOBox} */(this); box._procFullBox(); if (box.flags & 1) { box._procField('aux_info_type', 'uint', 32); box._procField('aux_info_type_parameter', 'uint', 32); } box._procField('default_sample_info_size', 'uint', 8); box._procField('sample_count', 'uint', 32); if (box.default_sample_info_size === 0) { box._procFieldArray('sample_info_size', box.sample_count, 'uint', 8); } }); // eslint-disable-next-line no-restricted-syntax const sencProcessor = function() { // eslint-disable-next-line no-invalid-this const box = /** @type {!ISOBox} */(this); box._procFullBox(); if (box.flags & 1) { box._procField('AlgorithmID', 'uint', 24); box._procField('IV_size', 'uint', 8); box._procFieldArray('KID', 16, 'uint', 8); } box._procField('sample_count', 'uint', 32); // eslint-disable-next-line no-restricted-syntax box._procEntries('entry', box.sample_count, function(entry) { // eslint-disable-next-line no-invalid-this const boxEntry = /** @type {!ISOBox} */(this); boxEntry._procEntryField(entry, 'InitializationVector', 'data', 8); if (boxEntry.flags & 2) { boxEntry._procEntryField(entry, 'NumberOfEntries', 'uint', 16); boxEntry._procSubEntries(entry, 'clearAndCryptedData', // eslint-disable-next-line no-restricted-syntax entry.NumberOfEntries, function(clearAndCryptedData) { // eslint-disable-next-line no-invalid-this const subBoxEntry = /** @type {!ISOBox} */(this); subBoxEntry._procEntryField(clearAndCryptedData, 'BytesOfClearData', 'uint', 16); subBoxEntry._procEntryField(clearAndCryptedData, 'BytesOfEncryptedData', 'uint', 32); }); } }); }; this.isoBoxer_.addBoxProcessor('senc', sencProcessor); // eslint-disable-next-line no-restricted-syntax this.isoBoxer_.addBoxProcessor('uuid', function() { const MssTransmuxer = shaka.transmuxer.MssTransmuxer; // eslint-disable-next-line no-invalid-this const box = /** @type {!ISOBox} */(this); let isSENC = true; for (let i = 0; i < 16; i++) { if (box.usertype[i] !== MssTransmuxer.UUID_SENC_[i]) { isSENC = false; } // Add support for other user types here } if (isSENC) { if (box._parsing) { // Convert this box to sepiff for later processing. // See processMediaSegment_ function. box.type = 'sepiff'; } // eslint-disable-next-line no-restricted-syntax, no-invalid-this sencProcessor.call(/** @type {!ISOBox} */(this)); } }); } /** * @override * @export */ destroy() { // Nothing } /** * Check if the mime type and the content type is supported. * @param {string} mimeType * @param {string=} contentType * @return {boolean} * @override * @export */ isSupported(mimeType, contentType) { const Capabilities = shaka.media.Capabilities; const isMss = mimeType.startsWith('mss/'); if (!this.isoBoxer_ || !isMss) { return false; } if (contentType) { return Capabilities.isTypeSupported( this.convertCodecs(contentType, mimeType)); } const ContentType = shaka.util.ManifestParserUtils.ContentType; const audioMime = this.convertCodecs(ContentType.AUDIO, mimeType); const videoMime = this.convertCodecs(ContentType.VIDEO, mimeType); return Capabilities.isTypeSupported(audioMime) || Capabilities.isTypeSupported(videoMime); } /** * @override * @export */ convertCodecs(contentType, mimeType) { return mimeType.replace('mss/', ''); } /** * @override * @export */ getOriginalMimeType() { return this.originalMimeType_; } /** * @override * @export */ transmux(data, stream, reference) { if (!reference) { // Init segment doesn't need transmux return Promise.resolve(shaka.util.BufferUtils.toUint8(data)); } if (!stream.mssPrivateData) { return Promise.reject(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MEDIA, shaka.util.Error.Code.MSS_MISSING_DATA_FOR_TRANSMUXING, reference ? reference.getUris()[0] : null)); } try { const transmuxedData = this.processMediaSegment_( data, stream, reference); return Promise.resolve(transmuxedData); } catch (exception) { if (exception instanceof shaka.util.Error) { return Promise.reject(exception); } return Promise.reject(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MEDIA, shaka.util.Error.Code.MSS_TRANSMUXING_FAILED, reference ? reference.getUris()[0] : null)); } } /** * Process a media segment from a data and stream. * @param {BufferSource} data * @param {shaka.extern.Stream} stream * @param {shaka.media.SegmentReference} reference * @return {!Uint8Array} * @private */ processMediaSegment_(data, stream, reference) { let i; /** @type {!ISOFile} */ const isoFile = this.isoBoxer_.parseBuffer(data); // Update track_Id in tfhd box const tfhd = isoFile.fetch('tfhd'); tfhd.track_ID = stream.id + 1; // Add tfdt box let tfdt = isoFile.fetch('tfdt'); const traf = isoFile.fetch('traf'); if (tfdt === null) { tfdt = this.isoBoxer_.createFullBox('tfdt', traf, tfhd); tfdt.version = 1; tfdt.flags = 0; const timescale = stream.mssPrivateData.timescale; const startTime = reference.startTime; tfdt.baseMediaDecodeTime = Math.floor(startTime * timescale); } const trun = isoFile.fetch('trun'); // Process tfxd boxes // This box provide absolute timestamp but we take the segment start // time for tfdt let tfxd = isoFile.fetch('tfxd'); if (tfxd) { tfxd._parent.boxes.splice(tfxd._parent.boxes.indexOf(tfxd), 1); tfxd = null; } let tfrf = isoFile.fetch('tfrf'); if (tfrf) { tfrf._parent.boxes.splice(tfrf._parent.boxes.indexOf(tfrf), 1); tfrf = null; } // If protected content in PIFF1.1 format // (sepiff box = Sample Encryption PIFF) // => convert sepiff box it into a senc box // => create saio and saiz boxes (if not already present) const sepiff = isoFile.fetch('sepiff'); if (sepiff !== null) { sepiff.type = 'senc'; sepiff.usertype = undefined; let saio = isoFile.fetch('saio'); if (saio === null) { // Create Sample Auxiliary Information Offsets Box box (saio) saio = this.isoBoxer_.createFullBox('saio', traf); saio.version = 0; saio.flags = 0; saio.entry_count = 1; saio.offset = [0]; const saiz = this.isoBoxer_.createFullBox('saiz', traf); saiz.version = 0; saiz.flags = 0; saiz.sample_count = sepiff.sample_count; saiz.default_sample_info_size = 0; saiz.sample_info_size = []; if (sepiff.flags & 0x02) { // Sub-sample encryption => set sample_info_size for each sample for (i = 0; i < sepiff.sample_count; i += 1) { // 10 = 8 (InitializationVector field size) + 2 // (subsample_count field size) // 6 = 2 (BytesOfClearData field size) + 4 // (BytesOfEncryptedData field size) const entry = /** @type {!ISOEntry} */ (sepiff.entry[i]); saiz.sample_info_size[i] = 10 + (6 * entry.NumberOfEntries); } } else { // No sub-sample encryption => set default // sample_info_size = InitializationVector field size (8) saiz.default_sample_info_size = 8; } } } // set tfhd.base-data-offset-present to false tfhd.flags &= 0xFFFFFE; // set tfhd.default-base-is-moof to true tfhd.flags |= 0x020000; // set trun.data-offset-present to true trun.flags |= 0x000001; // Update trun.data_offset field that corresponds to first data byte // (inside mdat box) const moof = isoFile.fetch('moof'); const length = moof.getLength(); trun.data_offset = length + 8; // Update saio box offset field according to new senc box offset const saio = isoFile.fetch('saio'); if (saio !== null) { const trafPosInMoof = this.getBoxOffset_(moof, 'traf'); const sencPosInTraf = this.getBoxOffset_(traf, 'senc'); // Set offset from begin fragment to the first IV field in senc box // 16 = box header (12) + sample_count field size (4) saio.offset[0] = trafPosInMoof + sencPosInTraf + 16; } return shaka.util.BufferUtils.toUint8(isoFile.write()); } /** * This function returns the offset of the 1st byte of a child box within * a container box. * * @param {ISOBox} parent * @param {string} type * @return {number} * @private */ getBoxOffset_(parent, type) { let offset = 8; for (let i = 0; i < parent.boxes.length; i++) { if (parent.boxes[i].type === type) { return offset; } offset += parent.boxes[i].size; } return offset; } }; /** * @private {!Uint8Array} */ shaka.transmuxer.MssTransmuxer.UUID_SENC_ = new Uint8Array([ 0xA2, 0x39, 0x4F, 0x52, 0x5A, 0x9B, 0x4F, 0x14, 0xA2, 0x44, 0x6C, 0x42, 0x7C, 0x64, 0x8D, 0xF4, ]); shaka.transmuxer.TransmuxerEngine.registerTransmuxer( 'mss/audio/mp4', () => new shaka.transmuxer.MssTransmuxer('mss/audio/mp4'), shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK); shaka.transmuxer.TransmuxerEngine.registerTransmuxer( 'mss/video/mp4', () => new shaka.transmuxer.MssTransmuxer('mss/video/mp4'), shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);