diff --git a/build/types/core b/build/types/core index 84410247db..3e9874db1a 100644 --- a/build/types/core +++ b/build/types/core @@ -91,6 +91,7 @@ +../../lib/util/media_ready_state_utils.js +../../lib/util/mime_utils.js +../../lib/util/mp4_box_parsers.js ++../../lib/util/mp4_generator.js +../../lib/util/mp4_parser.js +../../lib/util/multi_map.js +../../lib/util/networking.js diff --git a/build/types/mss b/build/types/mss index a9f330d2ec..e98b363e93 100644 --- a/build/types/mss +++ b/build/types/mss @@ -2,6 +2,5 @@ +../../lib/mss/content_protection.js +../../lib/mss/mss_parser.js -+../../lib/mss/mss_utils.js +../../lib/transmuxer/mss_transmuxer.js diff --git a/lib/mss/mss_parser.js b/lib/mss/mss_parser.js index b86ef6d933..e8e4ab3e36 100644 --- a/lib/mss/mss_parser.js +++ b/lib/mss/mss_parser.js @@ -15,14 +15,13 @@ goog.require('shaka.media.PresentationTimeline'); goog.require('shaka.media.SegmentIndex'); goog.require('shaka.media.SegmentReference'); goog.require('shaka.mss.ContentProtection'); -goog.require('shaka.mss.MssUtils'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.util.Error'); goog.require('shaka.util.ManifestParserUtils'); +goog.require('shaka.util.Mp4Generator'); goog.require('shaka.util.OperationManager'); goog.require('shaka.util.Timer'); goog.require('shaka.util.XmlUtils'); -goog.require('shaka.dependencies'); /** @@ -485,6 +484,13 @@ shaka.mss.MssParser = class { const audioSamplingRate = XmlUtils.parseAttr( qualityLevel, 'SamplingRate', XmlUtils.parsePositiveInt); + let duration = context.duration; + if (timeline.length) { + const start = timeline[0].start; + const end = timeline[timeline.length - 1].end; + duration = end - start; + } + /** @type {!shaka.extern.Stream} */ const stream = { id: id, @@ -519,7 +525,7 @@ shaka.mss.MssParser = class { tilesLayout: undefined, matchedStreams: [], mssPrivateData: { - duration: context.duration, + duration: duration, timescale: context.timescale, codecPrivateData: null, }, @@ -612,7 +618,16 @@ shaka.mss.MssParser = class { if (this.initSegmentDataByStreamId_.has(stream.id)) { initSegmentData = this.initSegmentDataByStreamId_.get(stream.id); } else { - initSegmentData = shaka.mss.MssUtils.generateInitSegment(stream); + const timescale = stream.mssPrivateData.timescale; + const duration = stream.mssPrivateData.duration; + let videoNalus = null; + if (stream.type == ContentType.VIDEO) { + const codecPrivateData = stream.mssPrivateData.codecPrivateData; + videoNalus = codecPrivateData.split('00000001').slice(1); + } + const mp4Generator = new shaka.util.Mp4Generator( + stream, timescale, duration, videoNalus); + initSegmentData = mp4Generator.initSegment(); this.initSegmentDataByStreamId_.set(stream.id, initSegmentData); } const initSegmentRef = new shaka.media.InitSegmentReference( @@ -624,7 +639,7 @@ shaka.mss.MssParser = class { initSegmentData); const segments = this.createSegments_(initSegmentRef, - stream, streamIndex, timeline, context); + stream, streamIndex, timeline); stream.segmentIndex = new shaka.media.SegmentIndex(segments); return Promise.resolve(); @@ -784,11 +799,10 @@ shaka.mss.MssParser = class { * @param {!shaka.extern.Stream} stream * @param {!Element} streamIndex * @param {!Array.} timeline - * @param {!shaka.mss.MssParser.Context} context * @return {!Array.} * @private */ - createSegments_(initSegmentRef, stream, streamIndex, timeline, context) { + createSegments_(initSegmentRef, stream, streamIndex, timeline) { const ManifestParserUtils = shaka.util.ManifestParserUtils; const url = streamIndex.getAttribute('Url'); goog.asserts.assert(url, 'Missing URL for segments'); @@ -810,7 +824,7 @@ shaka.mss.MssParser = class { initSegmentRef, /* timestampOffset= */ 0, /* appendWindowStart= */ 0, - /* appendWindowEnd= */ context.duration)); + /* appendWindowEnd= */ stream.mssPrivateData.duration)); } return segments; } @@ -1043,9 +1057,7 @@ shaka.mss.MssParser.Context; */ shaka.mss.MssParser.TimeRange; -if (shaka.dependencies.isoBoxer()) { - shaka.media.ManifestParser.registerParserByExtension( - 'ism', () => new shaka.mss.MssParser()); - shaka.media.ManifestParser.registerParserByMime( - 'application/vnd.ms-sstr+xml', () => new shaka.mss.MssParser()); -} +shaka.media.ManifestParser.registerParserByExtension( + 'ism', () => new shaka.mss.MssParser()); +shaka.media.ManifestParser.registerParserByMime( + 'application/vnd.ms-sstr+xml', () => new shaka.mss.MssParser()); diff --git a/lib/mss/mss_utils.js b/lib/mss/mss_utils.js deleted file mode 100644 index f753f55bfd..0000000000 --- a/lib/mss/mss_utils.js +++ /dev/null @@ -1,826 +0,0 @@ -/*! @license - * MSS Utils - * 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. - */ - -goog.provide('shaka.mss.MssUtils'); - -goog.require('goog.asserts'); -goog.require('shaka.util.BufferUtils'); -goog.require('shaka.util.Error'); -goog.require('shaka.util.ManifestParserUtils'); -goog.require('shaka.dependencies'); - - -/** - * @summary MSS processing utility functions. - */ -shaka.mss.MssUtils = class { - /** - * Generate a Init Segment (MP4) for a MSS stream. - * - * @param {shaka.extern.Stream} stream - * @return {!BufferSource} - */ - static generateInitSegment(stream) { - const MssUtils = shaka.mss.MssUtils; - const isoBoxer = shaka.dependencies.isoBoxer(); - goog.asserts.assert(isoBoxer, 'ISOBoxer should be defined.'); - const isoFile = isoBoxer.createFile(); - MssUtils.createFtypBox_(isoBoxer, isoFile); - MssUtils.createMoovBox_(isoBoxer, isoFile, stream); - return shaka.util.BufferUtils.toUint8(isoFile.write()); - } - - /** - * Create ftyp box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} isoFile - * @private - */ - static createFtypBox_(isoBoxer, isoFile) { - const ftyp = isoBoxer.createBox('ftyp', isoFile); - ftyp.major_brand = 'iso6'; - // is an informative integer for the minor version of the major brand - ftyp.minor_version = 1; - // is a list, to the end of the box, of brands isom, iso6 and msdh - ftyp.compatible_brands = []; - // => decimal ASCII value for isom - ftyp.compatible_brands[0] = 'isom'; - // => decimal ASCII value for iso6 - ftyp.compatible_brands[1] = 'iso6'; - // => decimal ASCII value for msdh - ftyp.compatible_brands[2] = 'msdh'; - } - - /** - * Create moov box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} isoFile - * @param {shaka.extern.Stream} stream - * @private - */ - static createMoovBox_(isoBoxer, isoFile, stream) { - const MssUtils = shaka.mss.MssUtils; - const ContentType = shaka.util.ManifestParserUtils.ContentType; - // moov box - const moov = isoBoxer.createBox('moov', isoFile); - // moov/mvhd - MssUtils.createMvhdBox_(isoBoxer, moov, stream); - // moov/trak - const trak = isoBoxer.createBox('trak', moov); - // moov/trak/tkhd - MssUtils.createTkhdBox_(isoBoxer, trak, stream); - // moov/trak/mdia - const mdia = isoBoxer.createBox('mdia', trak); - // moov/trak/mdia/mdhd - MssUtils.createMdhdBox_(isoBoxer, mdia, stream); - // moov/trak/mdia/hdlr - MssUtils.createHdlrBox_(isoBoxer, mdia, stream); - // moov/trak/mdia/minf - const minf = isoBoxer.createBox('minf', mdia); - switch (stream.type) { - case ContentType.VIDEO: - // moov/trak/mdia/minf/vmhd - MssUtils.createVmhdBox_(isoBoxer, minf); - break; - case ContentType.AUDIO: - // moov/trak/mdia/minf/smhd - MssUtils.createSmhdBox_(isoBoxer, minf); - break; - } - // moov/trak/mdia/minf/dinf - const dinf = isoBoxer.createBox('dinf', minf); - // moov/trak/mdia/minf/dinf/dref - MssUtils.createDrefBox_(isoBoxer, dinf); - // moov/trak/mdia/minf/stbl - const stbl = isoBoxer.createBox('stbl', minf); - // Create empty stts, stsc, stco and stsz boxes - // Use data field as for codem-isoboxer unknown boxes for setting - // fields value - // moov/trak/mdia/minf/stbl/stts - const stts = isoBoxer.createFullBox('stts', stbl); - // version = 0, flags = 0, entry_count = 0 - stts._data = [0, 0, 0, 0, 0, 0, 0, 0]; - // moov/trak/mdia/minf/stbl/stsc - const stsc = isoBoxer.createFullBox('stsc', stbl); - // version = 0, flags = 0, entry_count = 0 - stsc._data = [0, 0, 0, 0, 0, 0, 0, 0]; - // moov/trak/mdia/minf/stbl/stco - const stco = isoBoxer.createFullBox('stco', stbl); - // version = 0, flags = 0, entry_count = 0 - stco._data = [0, 0, 0, 0, 0, 0, 0, 0]; - // moov/trak/mdia/minf/stbl/stsz - const stsz = isoBoxer.createFullBox('stsz', stbl); - // version = 0, flags = 0, sample_size = 0, sample_count = 0 - stsz._data = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - // moov/trak/mdia/minf/stbl/stsd - MssUtils.createStsdBox_(isoBoxer, stbl, stream); - // moov/mvex - const mvex = isoBoxer.createBox('mvex', moov); - // moov/mvex/trex - MssUtils.createTrexBox_(isoBoxer, mvex, stream); - if (stream.encrypted) { - MssUtils.createProtectionSystemSpecificHeaderBox_(isoBoxer, moov, stream); - } - } - - /** - * Create mvhd box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} moov - * @param {shaka.extern.Stream} stream - * @private - */ - static createMvhdBox_(isoBoxer, moov, stream) { - const mvhd = isoBoxer.createFullBox('mvhd', moov); - // version = 1 in order to have 64bits duration value - mvhd.version = 1; - // the creation time of the presentation => ignore (set to 0) - mvhd.creation_time = 0; - // the most recent time the presentation was modified => ignore (set to 0) - mvhd.modification_time = 0; - // the time-scale for the entire presentation => 10000000 for MSS - const timescale = stream.mssPrivateData.timescale; - mvhd.timescale = timescale; - // the length of the presentation (in the indicated timescale) - const duration = stream.mssPrivateData.duration; - mvhd.duration = duration === Infinity ? - 0x1FFFFFFFFFFFFF : Math.round(duration * timescale); - // 16.16 number, '1.0' = normal playback - mvhd.rate = 1.0; - // 8.8 number, '1.0' = full volume - mvhd.volume = 1.0; - mvhd.reserved1 = 0; - mvhd.reserved2 = [0x0, 0x0]; - mvhd.matrix = [ - 1, 0, 0, // provides a transformation matrix for the video; - 0, 1, 0, // (u,v,w) are restricted here to (0,0,1) - 0, 0, 16384, - ]; - mvhd.pre_defined = [0, 0, 0, 0, 0, 0]; - // indicates a value to use for the track ID of the next track to be - // added to this presentation - mvhd.next_track_ID = (stream.id + 1) + 1; - } - - /** - * Create tkhd box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} trak - * @param {shaka.extern.Stream} stream - * @private - */ - static createTkhdBox_(isoBoxer, trak, stream) { - const tkhd = isoBoxer.createFullBox('tkhd', trak); - // version = 1 in order to have 64bits duration value - tkhd.version = 1; - // Track_enabled (0x000001): Indicates that the track is enabled - // Track_in_movie (0x000002): Indicates that the track is used in - // the presentation - // Track_in_preview (0x000004): Indicates that the track is used when - // previewing the presentation - tkhd.flags = 0x1 | 0x2 | 0x4; - // the creation time of the presentation => ignore (set to 0) - tkhd.creation_time = 0; - // the most recent time the presentation was modified => ignore (set to 0) - tkhd.modification_time = 0; - // uniquely identifies this track over the entire life-time of this - // presentation - tkhd.track_ID = (stream.id + 1); - tkhd.reserved1 = 0; - // the duration of this track (in the timescale indicated in the Movie - // Header Box) - const duration = stream.mssPrivateData.duration; - const timescale = stream.mssPrivateData.timescale; - tkhd.duration = duration === Infinity ? - 0x1FFFFFFFFFFFFF : Math.round(duration * timescale); - tkhd.reserved2 = [0x0, 0x0]; - // specifies the front-to-back ordering of video tracks; tracks with lower - // numbers are closer to the viewer => 0 since only one video track - tkhd.layer = 0; - // specifies a group or collection of tracks => ignore - tkhd.alternate_group = 0; - // '1.0' = full volume - tkhd.volume = 1.0; - tkhd.reserved3 = 0; - tkhd.matrix = [ - 1, 0, 0, // provides a transformation matrix for the video; - 0, 1, 0, // (u,v,w) are restricted here to (0,0,1) - 0, 0, 16384, - ]; - // visual presentation width - tkhd.width = stream.width; - // visual presentation height - tkhd.height = stream.height; - } - - /** - * Create mdhd box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} mdia - * @param {shaka.extern.Stream} stream - * @private - */ - static createMdhdBox_(isoBoxer, mdia, stream) { - const mdhd = isoBoxer.createFullBox('mdhd', mdia); - // version = 1 in order to have 64bits duration value - mdhd.version = 1; - // the creation time of the presentation => ignore (set to 0) - mdhd.creation_time = 0; - // the most recent time the presentation was modified => ignore (set to 0) - mdhd.modification_time = 0; - // the time-scale for the entire presentation - const timescale = stream.mssPrivateData.timescale; - mdhd.timescale = timescale; - // the duration of this media (in the scale of the timescale). - // If the duration cannot be determined then duration is set to all 1s. - const duration = stream.mssPrivateData.duration; - mdhd.duration = duration === Infinity ? - 0x1FFFFFFFFFFFFF : Math.round(duration * timescale); - // declares the language code for this media - mdhd.language = stream.language; - mdhd.pre_defined = 0; - } - - /** - * Create hdlr box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} mdia - * @param {shaka.extern.Stream} stream - * @private - */ - static createHdlrBox_(isoBoxer, mdia, stream) { - const ContentType = shaka.util.ManifestParserUtils.ContentType; - const hdlr = isoBoxer.createFullBox('hdlr', mdia); - hdlr.pre_defined = 0; - switch (stream.type) { - case ContentType.VIDEO: - hdlr.handler_type = 'vide'; - break; - case ContentType.AUDIO: - hdlr.handler_type = 'soun'; - break; - default: - hdlr.handler_type = 'meta'; - break; - } - hdlr.name = stream.originalId; - hdlr.reserved = [0, 0, 0]; - } - - /** - * Create vmhd box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} minf - * @private - */ - static createVmhdBox_(isoBoxer, minf) { - const vmhd = isoBoxer.createFullBox('vmhd', minf); - vmhd.flags = 1; - // specifies a composition mode for this video track, from the following - // enumerated set, which may be extended by derived specifications: - // copy = 0 copy over the existing image - vmhd.graphicsmode = 0; - // is a set of 3 colour values (red, green, blue) available for use by - // graphics modes - vmhd.opcolor = [0, 0, 0]; - } - - /** - * Create smhd box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} minf - * @private - */ - static createSmhdBox_(isoBoxer, minf) { - const smhd = isoBoxer.createFullBox('smhd', minf); - smhd.flags = 1; - // is a fixed-point 8.8 number that places mono audio tracks in a stereo - // space; 0 is centre (the normal value); full left is -1.0 and full - // right is 1.0. - smhd.balance = 0; - smhd.reserved = 0; - } - - /** - * Create dref box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} dinf - * @private - */ - static createDrefBox_(isoBoxer, dinf) { - const dref = isoBoxer.createFullBox('dref', dinf); - dref.entry_count = 1; - dref.entries = []; - const url = isoBoxer.createFullBox('url ', dref, false); - url.location = ''; - url.flags = 1; - dref.entries.push(url); - } - - /** - * Create stsd box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} stbl - * @param {shaka.extern.Stream} stream - * @private - */ - static createStsdBox_(isoBoxer, stbl, stream) { - const MssUtils = shaka.mss.MssUtils; - const ContentType = shaka.util.ManifestParserUtils.ContentType; - const stsd = isoBoxer.createFullBox('stsd', stbl); - stsd.entries = []; - switch (stream.type) { - case ContentType.VIDEO: - case ContentType.AUDIO: - stsd.entries.push(MssUtils.createSampleEntry_(isoBoxer, stsd, stream)); - break; - default: - break; - } - // is an integer that counts the actual entries - stsd.entry_count = stsd.entries.length; - } - - /** - * Create sample entry box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} stsd - * @param {shaka.extern.Stream} stream - * @private - */ - static createSampleEntry_(isoBoxer, stsd, stream) { - const MssUtils = shaka.mss.MssUtils; - const codec = stream.codecs.substring(0, stream.codecs.indexOf('.')); - switch (codec) { - case 'avc1': - return MssUtils.createAVCVisualSampleEntry_( - isoBoxer, stsd, codec, stream); - case 'mp4a': - return MssUtils.createMP4AudioSampleEntry_( - isoBoxer, stsd, codec, stream); - default: - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MEDIA, - shaka.util.Error.Code.MSS_TRANSMUXING_CODEC_UNKNOWN, - codec); - } - } - - /** - * Create AVC Visual Sample Entry box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} stsd - * @param {string} codec - * @param {shaka.extern.Stream} stream - * @private - */ - static createAVCVisualSampleEntry_(isoBoxer, stsd, codec, stream) { - const MssUtils = shaka.mss.MssUtils; - let avc1; - if (stream.encrypted) { - avc1 = isoBoxer.createBox('encv', stsd, false); - } else { - avc1 = isoBoxer.createBox('avc1', stsd, false); - } - // SampleEntry fields - avc1.reserved1 = [0x0, 0x0, 0x0, 0x0, 0x0, 0x0]; - avc1.data_reference_index = 1; - // VisualSampleEntry fields - avc1.pre_defined1 = 0; - avc1.reserved2 = 0; - avc1.pre_defined2 = [0, 0, 0]; - avc1.height = stream.height; - avc1.width = stream.width; - // 72 dpi - avc1.horizresolution = 72; - // 72 dpi - avc1.vertresolution = 72; - avc1.reserved3 = 0; - // 1 compressed video frame per sample - avc1.frame_count = 1; - avc1.compressorname = [ - 0x0A, 0x41, 0x56, 0x43, 0x20, 0x43, 0x6F, 0x64, // = 'AVC Coding'; - 0x69, 0x6E, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]; - // 0x0018 – images are in colour with no alpha. - avc1.depth = 0x0018; - avc1.pre_defined3 = 65535; - avc1.config = MssUtils.createAVC1ConfigurationRecord_(isoBoxer, stream); - if (stream.encrypted) { - // Create and add Protection Scheme Info Box - const sinf = isoBoxer.createBox('sinf', avc1); - // Create and add Original Format Box => indicate codec type of the - // encrypted content - MssUtils.createOriginalFormatBox_(isoBoxer, sinf, codec); - // Create and add Scheme Type box - MssUtils.createSchemeTypeBox_(isoBoxer, sinf); - // Create and add Scheme Information Box - MssUtils.createSchemeInformationBox_(isoBoxer, sinf, stream); - } - return avc1; - } - - /** - * Create AVC1 configuration record. - * - * @param {ISOBoxer} isoBoxer - * @param {shaka.extern.Stream} stream - * @private - */ - static createAVC1ConfigurationRecord_(isoBoxer, stream) { - const MssUtils = shaka.mss.MssUtils; - - const NALUTYPE_SPS = 7; - const NALUTYPE_PPS = 8; - - // length = 15 by default (0 SPS and 0 PPS) - let avcCLength = 15; - // First get all SPS and PPS from codecPrivateData - const sps = []; - const pps = []; - let AVCProfileIndication = 0; - let AVCLevelIndication = 0; - let profileCompatibility = 0; - const codecPrivateData = stream.mssPrivateData.codecPrivateData; - const nalus = codecPrivateData.split('00000001').slice(1); - for (let i = 0; i < nalus.length; i++) { - const naluBytes = MssUtils.hexStringToBuffer_(nalus[i]); - const naluType = naluBytes[0] & 0x1F; - switch (naluType) { - case NALUTYPE_SPS: - sps.push(naluBytes); - // 2 = sequenceParameterSetLength field length - avcCLength += naluBytes.length + 2; - break; - case NALUTYPE_PPS: - pps.push(naluBytes); - // 2 = pictureParameterSetLength field length - avcCLength += naluBytes.length + 2; - break; - default: - break; - } - } - // Get profile and level from SPS - if (sps.length > 0) { - AVCProfileIndication = sps[0][1]; - profileCompatibility = sps[0][2]; - AVCLevelIndication = sps[0][3]; - } - // Generate avcC buffer - const avcC = new Uint8Array(avcCLength); - let i = 0; - // length - avcC[i++] = (avcCLength & 0xFF000000) >> 24; - avcC[i++] = (avcCLength & 0x00FF0000) >> 16; - avcC[i++] = (avcCLength & 0x0000FF00) >> 8; - avcC[i++] = (avcCLength & 0x000000FF); - // type = 'avcC' - avcC.set([0x61, 0x76, 0x63, 0x43], i); - i += 4; - // configurationVersion = 1 - avcC[i++] = 1; - avcC[i++] = AVCProfileIndication; - avcC[i++] = profileCompatibility; - avcC[i++] = AVCLevelIndication; - // '11111' + lengthSizeMinusOne = 3 - avcC[i++] = 0xFF; - // '111' + numOfSequenceParameterSets - avcC[i++] = 0xE0 | sps.length; - for (let n = 0; n < sps.length; n++) { - avcC[i++] = (sps[n].length & 0xFF00) >> 8; - avcC[i++] = (sps[n].length & 0x00FF); - avcC.set(sps[n], i); - i += sps[n].length; - } - // numOfPictureParameterSets - avcC[i++] = pps.length; - for (let n = 0; n < pps.length; n++) { - avcC[i++] = (pps[n].length & 0xFF00) >> 8; - avcC[i++] = (pps[n].length & 0x00FF); - avcC.set(pps[n], i); - i += pps[n].length; - } - return avcC; - } - - /** - * Create MP4 Audio Sample Entry box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} stsd - * @param {string} codec - * @param {shaka.extern.Stream} stream - * @private - */ - static createMP4AudioSampleEntry_(isoBoxer, stsd, codec, stream) { - const MssUtils = shaka.mss.MssUtils; - // By default assumes stereo - const channelsCount = stream.channelsCount || 2; - // By default assumes 44.1khz - const audioSamplingRate = stream.audioSamplingRate || 44100; - let mp4a; - if (stream.encrypted) { - mp4a = isoBoxer.createBox('enca', stsd, false); - } else { - mp4a = isoBoxer.createBox('mp4a', stsd, false); - } - // SampleEntry fields - mp4a.reserved1 = [0x0, 0x0, 0x0, 0x0, 0x0, 0x0]; - mp4a.data_reference_index = 1; - // AudioSampleEntry fields - mp4a.reserved2 = [0x0, 0x0]; - mp4a.channelcount = channelsCount; - mp4a.samplesize = 16; - mp4a.pre_defined = 0; - mp4a.reserved_3 = 0; - mp4a.samplerate = audioSamplingRate << 16; - mp4a.esds = MssUtils.createMPEG4AACESDescriptor_(isoBoxer, stream); - if (stream.encrypted) { - // Create and add Protection Scheme Info Box - const sinf = isoBoxer.createBox('sinf', mp4a); - // Create and add Original Format Box => indicate codec type of the - // encrypted content - MssUtils.createOriginalFormatBox_(isoBoxer, sinf, codec); - // Create and add Scheme Type box - MssUtils.createSchemeTypeBox_(isoBoxer, sinf); - // Create and add Scheme Information Box - MssUtils.createSchemeInformationBox_(isoBoxer, sinf, stream); - } - return mp4a; - } - - /** - * Create ESDS descriptor. - * - * @param {ISOBoxer} isoBoxer - * @param {shaka.extern.Stream} stream - * @private - */ - static createMPEG4AACESDescriptor_(isoBoxer, stream) { - const MssUtils = shaka.mss.MssUtils; - const codecPrivateData = stream.mssPrivateData.codecPrivateData; - goog.asserts.assert(codecPrivateData, 'Missing CodecPrivateData'); - // AudioSpecificConfig (see ISO/IEC 14496-3, subpart 1) => corresponds to - // hex bytes contained in 'codecPrivateData' field - const audioSpecificConfig = MssUtils.hexStringToBuffer_(codecPrivateData); - - // ESDS length = esds box header length (= 12) + - // ES_Descriptor header length (= 5) + - // DecoderConfigDescriptor header length (= 15) + - // decoderSpecificInfo header length (= 2) + - // AudioSpecificConfig length (= codecPrivateData length) - const esdsLength = 34 + audioSpecificConfig.length; - const esds = new Uint8Array(esdsLength); - let i = 0; - // esds box - // esds box length - esds[i++] = (esdsLength & 0xFF000000) >> 24; - esds[i++] = (esdsLength & 0x00FF0000) >> 16; - esds[i++] = (esdsLength & 0x0000FF00) >> 8; - esds[i++] = (esdsLength & 0x000000FF); - // type = 'esds' - esds.set([0x65, 0x73, 0x64, 0x73], i); - i += 4; - // version = 0, flags = 0 - esds.set([0, 0, 0, 0], i); - i += 4; - // ES_Descriptor (see ISO/IEC 14496-1 (Systems)) - // tag = 0x03 (ES_DescrTag) - esds[i++] = 0x03; - // size - esds[i++] = 20 + audioSpecificConfig.length; - // ES_ID = track_id - esds[i++] = ((stream.id + 1) & 0xFF00) >> 8; - esds[i++] = ((stream.id + 1) & 0x00FF); - // flags and streamPriority - esds[i++] = 0; - // DecoderConfigDescriptor (see ISO/IEC 14496-1 (Systems)) - // tag = 0x04 (DecoderConfigDescrTag) - esds[i++] = 0x04; - // size - esds[i++] = 15 + audioSpecificConfig.length; - // objectTypeIndication = 0x40 (MPEG-4 AAC) - esds[i++] = 0x40; - // streamType = 0x05 (Audiostream) - esds[i] = 0x05 << 2; - // upStream = 0 - esds[i] |= 0 << 1; - // reserved = 1 - esds[i++] |= 1; - // buffersizeDB = undefined - esds[i++] = 0xFF; - esds[i++] = 0xFF; - esds[i++] = 0xFF; - const bandwidth = stream.bandwidth || 0; - // maxBitrate - esds[i++] = (bandwidth & 0xFF000000) >> 24; - esds[i++] = (bandwidth & 0x00FF0000) >> 16; - esds[i++] = (bandwidth & 0x0000FF00) >> 8; - esds[i++] = (bandwidth & 0x000000FF); - // avgbitrate - esds[i++] = (bandwidth & 0xFF000000) >> 24; - esds[i++] = (bandwidth & 0x00FF0000) >> 16; - esds[i++] = (bandwidth & 0x0000FF00) >> 8; - esds[i++] = (bandwidth & 0x000000FF); - - // DecoderSpecificInfo (see ISO/IEC 14496-1 (Systems)) - // tag = 0x05 (DecSpecificInfoTag) - esds[i++] = 0x05; - // size - esds[i++] = audioSpecificConfig.length; - // AudioSpecificConfig bytes - esds.set(audioSpecificConfig, i); - - return esds; - } - - /** - * Create frma box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} sinf - * @param {string} codec - * @private - */ - static createOriginalFormatBox_(isoBoxer, sinf, codec) { - const MssUtils = shaka.mss.MssUtils; - const frma = isoBoxer.createBox('frma', sinf); - frma.data_format = MssUtils.stringToCharCode_(codec); - } - - /** - * Create schm box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} sinf - * @private - */ - static createSchemeTypeBox_(isoBoxer, sinf) { - const schm = isoBoxer.createFullBox('schm', sinf); - schm.flags = 0; - schm.version = 0; - // 'cenc' => common encryption - schm.scheme_type = 0x63656E63; - // version set to 0x00010000 (Major version 1, Minor version 0) - schm.scheme_version = 0x00010000; - } - - /** - * Create schi box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} sinf - * @param {shaka.extern.Stream} stream - * @private - */ - static createSchemeInformationBox_(isoBoxer, sinf, stream) { - const MssUtils = shaka.mss.MssUtils; - const schi = isoBoxer.createBox('schi', sinf); - // Create and add Track Encryption Box - MssUtils.createTrackEncryptionBox_(isoBoxer, schi, stream); - } - - /** - * Create tenc box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} schi - * @param {shaka.extern.Stream} stream - * @private - */ - static createTrackEncryptionBox_(isoBoxer, schi, stream) { - const tenc = isoBoxer.createFullBox('tenc', schi); - tenc.flags = 0; - tenc.version = 0; - tenc.default_IsEncrypted = 0x1; - tenc.default_IV_size = 8; - let defaultKID = [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0]; - for (const drmInfo of stream.drmInfos) { - if (drmInfo && drmInfo.keyId && drmInfo.keyIds.size) { - for (const keyId of drmInfo.keyIds) { - defaultKID = keyId; - } - } - } - tenc.default_KID = defaultKID; - } - - /** - * Create trex box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} moov - * @param {shaka.extern.Stream} stream - * @private - */ - static createTrexBox_(isoBoxer, moov, stream) { - const trex = isoBoxer.createFullBox('trex', moov); - trex.track_ID = (stream.id + 1); - trex.default_sample_description_index = 1; - trex.default_sample_duration = 0; - trex.default_sample_size = 0; - trex.default_sample_flags = 0; - } - - /** - * Create PSSH box. - * - * @param {ISOBoxer} isoBoxer - * @param {ISOBoxer} moov - * @param {shaka.extern.Stream} stream - * @private - */ - static createProtectionSystemSpecificHeaderBox_(isoBoxer, moov, stream) { - const BufferUtils = shaka.util.BufferUtils; - for (const drmInfo of stream.drmInfos) { - if (!drmInfo.initData) { - continue; - } - for (const initData of drmInfo.initData) { - const initDataBuffer = BufferUtils.toArrayBuffer(initData.initData); - const parsedBuffer = isoBoxer.parseBuffer(initDataBuffer); - const pssh = parsedBuffer.fetch('pssh'); - if (pssh) { - isoBoxer.Utils.appendBox(moov, pssh); - } - } - } - } - - /** - * Convert a hex string to buffer. - * - * @param {string} str - * @return {Uint8Array} - * @private - */ - static hexStringToBuffer_(str) { - const buf = new Uint8Array(str.length / 2); - for (let i = 0; i < str.length / 2; i += 1) { - buf[i] = parseInt(String(str[i * 2] + str[i * 2 + 1]), 16); - } - return buf; - } - - /** - * Convert a string to char code. - * - * @param {string} str - * @return {number} - * @private - */ - static stringToCharCode_(str) { - let code = 0; - for (let i = 0; i < str.length; i += 1) { - code |= str.charCodeAt(i) << ((str.length - i - 1) * 8); - } - return code; - } -}; - diff --git a/lib/util/mp4_generator.js b/lib/util/mp4_generator.js new file mode 100644 index 0000000000..b86b83d9b0 --- /dev/null +++ b/lib/util/mp4_generator.js @@ -0,0 +1,947 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.util.Mp4Generator'); + +goog.require('goog.asserts'); +goog.require('shaka.util.ManifestParserUtils'); +goog.require('shaka.util.Uint8ArrayUtils'); + + +/** + * @export + */ +shaka.util.Mp4Generator = class { + /** + * @param {shaka.extern.Stream} stream + * @param {number} timescale + * @param {?number} duration + * @param {?Array.} videoNalus + */ + constructor(stream, timescale, duration, videoNalus) { + shaka.util.Mp4Generator.initStaticProperties_(); + + /** @private {shaka.extern.Stream} */ + this.stream_ = stream; + + /** @private {number} */ + this.timescale_ = timescale; + + /** @private {number} */ + this.duration_ = duration || 0xffffffff; + if (this.duration_ === Infinity) { + this.duration_ = 0xffffffff; + } + + /** @private {Array.} */ + this.videoNalus_ = videoNalus || []; + } + + /** + * Generate a Init Segment (MP4). + * + * @return {!Uint8Array} + */ + initSegment() { + const Mp4Generator = shaka.util.Mp4Generator; + const movie = this.moov_(); + const length = Mp4Generator.FTYP_.byteLength + movie.byteLength; + const result = new Uint8Array(length); + result.set(Mp4Generator.FTYP_); + result.set(movie, Mp4Generator.FTYP_.byteLength); + return result; + } + + /** + * Generate a MOOV box + * + * @return {!Uint8Array} + * @private + */ + moov_() { + const Mp4Generator = shaka.util.Mp4Generator; + return Mp4Generator.box('moov', + this.mvhd_(), this.trak_(), this.mvex_(), this.pssh_()); + } + + /** + * Generate a MVHD box + * + * @return {!Uint8Array} + * @private + */ + mvhd_() { + const Mp4Generator = shaka.util.Mp4Generator; + const duration = this.duration_ * this.timescale_; + const upperWordDuration = + Math.floor(duration / (Mp4Generator.UINT32_MAX_ + 1)); + const lowerWordDuration = + Math.floor(duration % (Mp4Generator.UINT32_MAX_ + 1)); + const bytes = new Uint8Array([ + 0x01, // version 1 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, // creation_time + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, // modification_time + ...this.breakNumberIntoBytes_(this.timescale_, 4), // timescale + ...this.breakNumberIntoBytes_(upperWordDuration, 4), + ...this.breakNumberIntoBytes_(lowerWordDuration, 4), // duration + 0x00, 0x01, 0x00, 0x00, // 1.0 rate + 0x01, 0x00, // 1.0 volume + 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0xff, 0xff, 0xff, 0xff, // next_track_ID + ]); + return Mp4Generator.box('mvhd', bytes); + } + + /** + * Generate a TRAK box + * + * @return {!Uint8Array} + * @private + */ + trak_() { + const Mp4Generator = shaka.util.Mp4Generator; + return Mp4Generator.box('trak', this.tkhd_(), this.mdia_()); + } + + /** + * Generate a TKHD box + * + * @return {!Uint8Array} + * @private + */ + tkhd_() { + const Mp4Generator = shaka.util.Mp4Generator; + const id = this.stream_.id + 1; + const width = this.stream_.width || 0; + const height = this.stream_.height || 0; + const duration = this.duration_ * this.timescale_; + const upperWordDuration = + Math.floor(duration / (Mp4Generator.UINT32_MAX_ + 1)); + const lowerWordDuration = + Math.floor(duration % (Mp4Generator.UINT32_MAX_ + 1)); + const bytes = new Uint8Array([ + 0x01, // version 1 + 0x00, 0x00, 0x07, // flags + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, // creation_time + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, // modification_time + ...this.breakNumberIntoBytes_(id, 4), // track_ID + 0x00, 0x00, 0x00, 0x00, // reserved + ...this.breakNumberIntoBytes_(upperWordDuration, 4), + ...this.breakNumberIntoBytes_(lowerWordDuration, 4), // duration + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, // layer + 0x00, 0x00, // alternate_group + 0x00, 0x00, // non-audio track volume + 0x00, 0x00, // reserved + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix + ...this.breakNumberIntoBytes_(width, 2), + 0x00, 0x00, // width + ...this.breakNumberIntoBytes_(height, 2), + 0x00, 0x00, // height + ]); + return Mp4Generator.box('tkhd', bytes); + } + + /** + * Generate a MDIA box + * + * @return {!Uint8Array} + * @private + */ + mdia_() { + const Mp4Generator = shaka.util.Mp4Generator; + return Mp4Generator.box( + 'mdia', this.mdhd_(), this.hdlr_(), this.minf_()); + } + + /** + * Generate a MDHD box + * + * @return {!Uint8Array} + * @private + */ + mdhd_() { + const Mp4Generator = shaka.util.Mp4Generator; + const duration = this.duration_ * this.timescale_; + const upperWordDuration = + Math.floor(duration / (Mp4Generator.UINT32_MAX_ + 1)); + const lowerWordDuration = + Math.floor(duration % (Mp4Generator.UINT32_MAX_ + 1)); + const language = this.stream_.language; + const languageNumber = ((language.charCodeAt(0) - 0x60) << 10) | + ((language.charCodeAt(1) - 0x60) << 5) | + ((language.charCodeAt(2) - 0x60)); + const bytes = new Uint8Array([ + 0x01, // version 1 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, // creation_time + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, // modification_time + ...this.breakNumberIntoBytes_(this.timescale_, 4), // timescale + ...this.breakNumberIntoBytes_(upperWordDuration, 4), + ...this.breakNumberIntoBytes_(lowerWordDuration, 4), // duration + ...this.breakNumberIntoBytes_(languageNumber, 2), // language + 0x00, 0x00, + ]); + return Mp4Generator.box('mdhd', bytes); + } + + /** + * Generate a HDLR box + * + * @return {!Uint8Array} + * @private + */ + hdlr_() { + const Mp4Generator = shaka.util.Mp4Generator; + const ContentType = shaka.util.ManifestParserUtils.ContentType; + let bytes = new Uint8Array([]); + switch (this.stream_.type) { + case ContentType.VIDEO: + bytes = Mp4Generator.HDLR_TYPES_.video; + break; + case ContentType.AUDIO: + bytes = Mp4Generator.HDLR_TYPES_.audio; + break; + } + return Mp4Generator.box('hdlr', bytes); + } + + /** + * Generate a MINF box + * + * @return {!Uint8Array} + * @private + */ + minf_() { + const Mp4Generator = shaka.util.Mp4Generator; + const ContentType = shaka.util.ManifestParserUtils.ContentType; + switch (this.stream_.type) { + case ContentType.VIDEO: + return Mp4Generator.box( + 'minf', Mp4Generator.box('vmhd', Mp4Generator.VMHD_), + Mp4Generator.DINF_, this.stbl_()); + case ContentType.AUDIO: + return Mp4Generator.box( + 'minf', Mp4Generator.box('smhd', Mp4Generator.SMHD_), + Mp4Generator.DINF_, this.stbl_()); + } + return new Uint8Array([]); + } + + /** + * Generate a STBL box + * + * @return {!Uint8Array} + * @private + */ + stbl_() { + const Mp4Generator = shaka.util.Mp4Generator; + return Mp4Generator.box( + 'stbl', + this.stsd_(), + Mp4Generator.box('stts', Mp4Generator.STTS_), + Mp4Generator.box('stsc', Mp4Generator.STSC_), + Mp4Generator.box('stsz', Mp4Generator.STSZ_), + Mp4Generator.box('stco', Mp4Generator.STCO_)); + } + + /** + * Generate a STSD box + * + * @return {!Uint8Array} + * @private + */ + stsd_() { + const Mp4Generator = shaka.util.Mp4Generator; + const ContentType = shaka.util.ManifestParserUtils.ContentType; + let bytes = new Uint8Array([]); + switch (this.stream_.type) { + case ContentType.VIDEO: + bytes = this.avc1_(); + break; + case ContentType.AUDIO: + if (this.stream_.mimeType === 'audio/mpeg' || + this.stream_.codecs.includes('mp3') || + this.stream_.codecs.includes('mp4a.40.34')) { + bytes = this.mp3_(); + } else { + bytes = this.mp4a_(); + } + break; + } + return Mp4Generator.box('stsd', Mp4Generator.STSD_, bytes); + } + + /** + * Generate a AVC1 box + * + * @return {!Uint8Array} + * @private + */ + avc1_() { + const Mp4Generator = shaka.util.Mp4Generator; + + const NALUTYPE_SPS = 7; + const NALUTYPE_PPS = 8; + + const width = this.stream_.width || 0; + const height = this.stream_.height || 0; + + // length = 7 by default (0 SPS and 0 PPS) + let avcCLength = 7; + + // First get all SPS and PPS from nalus + const sps = []; + const pps = []; + let AVCProfileIndication = 0; + let AVCLevelIndication = 0; + let profileCompatibility = 0; + for (let i = 0; i < this.videoNalus_.length; i++) { + const naluBytes = this.hexStringToBuffer_(this.videoNalus_[i]); + const naluType = naluBytes[0] & 0x1F; + switch (naluType) { + case NALUTYPE_SPS: + sps.push(naluBytes); + // 2 = sequenceParameterSetLength field length + avcCLength += naluBytes.length + 2; + break; + case NALUTYPE_PPS: + pps.push(naluBytes); + // 2 = pictureParameterSetLength field length + avcCLength += naluBytes.length + 2; + break; + default: + break; + } + } + // Get profile and level from SPS + if (sps.length > 0) { + AVCProfileIndication = sps[0][1]; + profileCompatibility = sps[0][2]; + AVCLevelIndication = sps[0][3]; + } + + // Generate avcC buffer + const avcCBytes = new Uint8Array(avcCLength); + let i = 0; + // configurationVersion = 1 + avcCBytes[i++] = 1; + avcCBytes[i++] = AVCProfileIndication; + avcCBytes[i++] = profileCompatibility; + avcCBytes[i++] = AVCLevelIndication; + // '11111' + lengthSizeMinusOne = 3 + avcCBytes[i++] = 0xFF; + // '111' + numOfSequenceParameterSets + avcCBytes[i++] = 0xE0 | sps.length; + for (let n = 0; n < sps.length; n++) { + avcCBytes[i++] = (sps[n].length & 0xFF00) >> 8; + avcCBytes[i++] = (sps[n].length & 0x00FF); + avcCBytes.set(sps[n], i); + i += sps[n].length; + } + // numOfPictureParameterSets + avcCBytes[i++] = pps.length; + for (let n = 0; n < pps.length; n++) { + avcCBytes[i++] = (pps[n].length & 0xFF00) >> 8; + avcCBytes[i++] = (pps[n].length & 0x00FF); + avcCBytes.set(pps[n], i); + i += pps[n].length; + } + const avcCBox = Mp4Generator.box('avcC', avcCBytes); + + const avc1Bytes = new Uint8Array([ + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // data_reference_index + 0x00, 0x00, // pre_defined + 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // pre_defined + ...this.breakNumberIntoBytes_(width, 2), // width + ...this.breakNumberIntoBytes_(height, 2), // height + 0x00, 0x48, 0x00, 0x00, // horizresolution + 0x00, 0x48, 0x00, 0x00, // vertresolution + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // frame_count + 0x13, + 0x76, 0x69, 0x64, 0x65, + 0x6f, 0x6a, 0x73, 0x2d, + 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x69, 0x62, 0x2d, + 0x68, 0x6c, 0x73, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, // compressorname + 0x00, 0x18, // depth = 24 + 0x11, 0x11, // pre_defined = -1 + ]); + + let sinfBox = new Uint8Array([]); + if (this.stream_.encrypted) { + sinfBox = this.sinf_(); + } + + let boxName = 'avc1'; + if (this.stream_.encrypted) { + boxName = 'encv'; + } + return Mp4Generator.box(boxName, avc1Bytes, avcCBox, sinfBox); + } + + /** + * Generate a .MP3 box + * + * @return {!Uint8Array} + * @private + */ + mp3_() { + const Mp4Generator = shaka.util.Mp4Generator; + const channelsCount = this.stream_.channelsCount || 2; + const audioSamplingRate = this.stream_.audioSamplingRate || 44100; + const bytes = new Uint8Array([ + 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // data_reference_index + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, + channelsCount, // channelcount + 0x00, 0x10, // sampleSize:16bits + 0x00, 0x00, 0x00, 0x00, // reserved2 + ...this.breakNumberIntoBytes_(audioSamplingRate, 2), // Sample Rate + 0x00, 0x00, + ]); + return Mp4Generator.box('.mp3', bytes); + } + + /** + * Generate a MP4A box + * + * @return {!Uint8Array} + * @private + */ + mp4a_() { + const Mp4Generator = shaka.util.Mp4Generator; + const channelsCount = this.stream_.channelsCount || 2; + const audioSamplingRate = this.stream_.audioSamplingRate || 44100; + const bytes = new Uint8Array([ + 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // data_reference_index + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, + channelsCount, // channelcount + 0x00, 0x10, // sampleSize:16bits + 0x00, 0x00, 0x00, 0x00, // reserved2 + ...this.breakNumberIntoBytes_(audioSamplingRate, 2), // Sample Rate + 0x00, 0x00, + ]); + const esdsBox = Mp4Generator.box('esds', this.esds_()); + + let sinfBox = new Uint8Array([]); + if (this.stream_.encrypted) { + sinfBox = this.sinf_(); + } + + let boxName = 'mp4a'; + if (this.stream_.encrypted) { + boxName = 'enca'; + } + return Mp4Generator.box(boxName, bytes, esdsBox, sinfBox); + } + + /** + * Generate a ESDS box + * + * @return {!Uint8Array} + * @private + */ + esds_() { + const ContentType = shaka.util.ManifestParserUtils.ContentType; + + const id = this.stream_.id + 1; + const bandwidth = this.stream_.bandwidth || 0; + const channelsCount = this.stream_.channelsCount || 2; + const audioSamplingRate = this.stream_.audioSamplingRate || 44100; + + const audioCodec = shaka.util.ManifestParserUtils.guessCodecs( + ContentType.AUDIO, this.stream_.codecs.split(',')); + + const samplingFrequencyIndex = { + 96000: 0x0, + 88200: 0x1, + 64000: 0x2, + 48000: 0x3, + 44100: 0x4, + 32000: 0x5, + 24000: 0x6, + 22050: 0x7, + 16000: 0x8, + 12000: 0x9, + 11025: 0xA, + 8000: 0xB, + 7350: 0xC, + }; + + let indexFreq = samplingFrequencyIndex[audioSamplingRate]; + // In HE AAC Sampling frequence equals to SamplingRate * 2 + if (audioCodec === 'mp4a.40.5' || audioCodec === 'mp4a.40.29') { + indexFreq = samplingFrequencyIndex[audioSamplingRate * 2]; + } + + const audioObjectType = parseInt(audioCodec.split('.').pop(), 10); + + return new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + + // ES_Descriptor + 0x03, // tag, ES_DescrTag + 0x19, // length + ...this.breakNumberIntoBytes_(id, 2), // ES_ID + 0x00, // streamDependenceFlag, URL_flag, reserved, streamPriority + + // DecoderConfigDescriptor + 0x04, // tag, DecoderConfigDescrTag + 0x11, // length + 0x40, // object type + 0x15, // streamType + 0x00, 0x06, 0x00, // bufferSizeDB + ...this.breakNumberIntoBytes_(bandwidth, 4), // maxBitrate + ...this.breakNumberIntoBytes_(bandwidth, 4), // avgBitrate + // DecoderSpecificInfo + 0x05, // tag, DecoderSpecificInfoTag + 0x02, // length + // ISO/IEC 14496-3, AudioSpecificConfig + // for samplingFrequencyIndex see + // ISO/IEC 13818-7:2006, 8.1.3.2.2, Table 35 + (audioObjectType << 3) | (indexFreq >>> 1), + (indexFreq << 7) | (channelsCount << 3), + 0x06, 0x01, 0x02, // GASpecificConfig + ]); + } + + /** + * Generate a MVEX box + * + * @return {!Uint8Array} + * @private + */ + mvex_() { + const Mp4Generator = shaka.util.Mp4Generator; + return Mp4Generator.box('mvex', this.trex_()); + } + + /** + * Generate a TREX box + * + * @return {!Uint8Array} + * @private + */ + trex_() { + const Mp4Generator = shaka.util.Mp4Generator; + const id = this.stream_.id + 1; + const bytes = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + ...this.breakNumberIntoBytes_(id, 4), // track_ID + 0x00, 0x00, 0x00, 0x01, // default_sample_description_index + 0x00, 0x00, 0x00, 0x00, // default_sample_duration + 0x00, 0x00, 0x00, 0x00, // default_sample_size + 0x00, 0x01, 0x00, 0x01, // default_sample_flags + ]); + return Mp4Generator.box('trex', bytes); + } + + /** + * Generate a PSSH box + * + * @return {!Uint8Array} + * @private + */ + pssh_() { + let boxes = new Uint8Array([]); + if (!this.stream_.encrypted) { + return boxes; + } + + for (const drmInfo of this.stream_.drmInfos) { + if (!drmInfo.initData) { + continue; + } + for (const initData of drmInfo.initData) { + boxes = shaka.util.Uint8ArrayUtils.concat(boxes, initData.initData); + } + } + return boxes; + } + + /** + * Generate a SINF box + * + * @return {!Uint8Array} + * @private + */ + sinf_() { + const Mp4Generator = shaka.util.Mp4Generator; + return Mp4Generator.box('sinf', + this.frma_(), this.schm_(), this.schi_()); + } + + /** + * Generate a FRMA box + * + * @return {!Uint8Array} + * @private + */ + frma_() { + const codec = this.stream_.codecs.substring( + 0, this.stream_.codecs.indexOf('.')); + const Mp4Generator = shaka.util.Mp4Generator; + const codecNumber = this.stringToCharCode_(codec); + const bytes = new Uint8Array([ + ...this.breakNumberIntoBytes_(codecNumber, 4), + ]); + return Mp4Generator.box('frma', bytes); + } + + /** + * Generate a SCHM box + * + * @return {!Uint8Array} + * @private + */ + schm_() { + const Mp4Generator = shaka.util.Mp4Generator; + const bytes = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x63, 0x65, 0x6e, 0x63, // Scheme: cenc + 0x00, 0x01, 0x00, 0x00, // Scheme version: 1.0 + ]); + return Mp4Generator.box('schm', bytes); + } + + /** + * Generate a SCHI box + * + * @return {!Uint8Array} + * @private + */ + schi_() { + const Mp4Generator = shaka.util.Mp4Generator; + return Mp4Generator.box('schi', this.tenc_()); + } + + /** + * Generate a TENC box + * + * @return {!Uint8Array} + * @private + */ + tenc_() { + // Default key ID: all zeros (dummy) + let defaultKeyId = new Uint8Array([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]); + for (const drmInfo of this.stream_.drmInfos) { + if (drmInfo && drmInfo.keyIds && drmInfo.keyIds.size) { + for (const keyId of drmInfo.keyIds) { + defaultKeyId = this.hexStringToBuffer_(keyId); + } + } + } + + const Mp4Generator = shaka.util.Mp4Generator; + const bytes = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, // Reserved fields + 0x01, // Default protected: true + 0x08, // Default per-sample IV size: 8 + ]); + goog.asserts.assert(defaultKeyId, 'Default KID should be non-null'); + return Mp4Generator.box('tenc', bytes, defaultKeyId); + } + + /** + * @param {number} number + * @param {number} numBytes + * @return {!Array.} + * @private + */ + breakNumberIntoBytes_(number, numBytes) { + const bytes = []; + for (let byte = numBytes - 1; byte >= 0; byte--) { + bytes.push((number >> (8 * byte)) & 0xff); + } + return bytes; + } + + /** + * Convert a hex string to buffer. + * + * @param {string} str + * @return {Uint8Array} + * @private + */ + hexStringToBuffer_(str) { + const buf = new Uint8Array(str.length / 2); + for (let i = 0; i < str.length / 2; i += 1) { + buf[i] = parseInt(String(str[i * 2] + str[i * 2 + 1]), 16); + } + return buf; + } + + /** + * Convert a string to char code. + * + * @param {string} str + * @return {number} + * @private + */ + stringToCharCode_(str) { + let code = 0; + for (let i = 0; i < str.length; i += 1) { + code |= str.charCodeAt(i) << ((str.length - i - 1) * 8); + } + return code; + } + + /** + * @private + */ + static initStaticProperties_() { + const Mp4Generator = shaka.util.Mp4Generator; + if (Mp4Generator.initializated_) { + return; + } + + Mp4Generator.initializated_ = true; + + const majorBrand = new Uint8Array([105, 115, 111, 109]); // isom + const avc1Brand = new Uint8Array([97, 118, 99, 49]); // avc1 + const minorVersion = new Uint8Array([0, 0, 0, 1]); + + Mp4Generator.FTYP_ = Mp4Generator.box( + 'ftyp', majorBrand, minorVersion, majorBrand, avc1Brand); + const drefBox = Mp4Generator.box('dref', Mp4Generator.DREF_); + Mp4Generator.DINF_ = Mp4Generator.box('dinf', drefBox); + } + + /** + * Generate a box + * + * @param {string} boxName + * @param {...!Uint8Array} payload + * @return {!Uint8Array} + */ + static box(boxName, ...payload) { + let type = shaka.util.Mp4Generator.BOX_TYPES_[boxName]; + if (!type) { + type = [ + boxName.charCodeAt(0), + boxName.charCodeAt(1), + boxName.charCodeAt(2), + boxName.charCodeAt(3), + ]; + shaka.util.Mp4Generator.BOX_TYPES_[boxName] = type; + } + // make the header for the box + let size = 8; + // calculate the total size we need to allocate + for (let i = payload.length - 1; i >= 0; i--) { + size += payload[i].byteLength; + } + const result = new Uint8Array(size); + result[0] = (size >> 24) & 0xff; + result[1] = (size >> 16) & 0xff; + result[2] = (size >> 8) & 0xff; + result[3] = size & 0xff; + result.set(type, 4); + + // copy the payload into the result + for (let i = 0, pointer = 8; i < payload.length; i++) { + // copy payload[i] array @ offset pointer + result.set(payload[i], pointer); + pointer += payload[i].byteLength; + } + return result; + } +}; + +/** + * @private {boolean} + */ +shaka.util.Mp4Generator.initializated_ = false; + + +/** + * @private {number} + */ +shaka.util.Mp4Generator.UINT32_MAX_ = Math.pow(2, 32) - 1; + +/** + * @private {!Object.>} + */ +shaka.util.Mp4Generator.BOX_TYPES_ = {}; + +/** + * @private {{video: !Uint8Array, audio: !Uint8Array}} + */ +shaka.util.Mp4Generator.HDLR_TYPES_ = { + video: new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x56, 0x69, 0x64, 0x65, + 0x6f, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x72, 0x00, // name: 'VideoHandler' + ]), + audio: new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun' + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x53, 0x6f, 0x75, 0x6e, + 0x64, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x72, 0x00, // name: 'SoundHandler' + ]), +}; + +/** + * @private {!Uint8Array} + */ +shaka.util.Mp4Generator.STTS_ = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // entry_count +]); + +/** + * @private {!Uint8Array} + */ +shaka.util.Mp4Generator.STSC_ = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // entry_count +]); + +/** + * @private {!Uint8Array} + */ +shaka.util.Mp4Generator.STCO_ = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // entry_count +]); + +/** + * @private {!Uint8Array} + */ +shaka.util.Mp4Generator.STSZ_ = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // sample_size + 0x00, 0x00, 0x00, 0x00, // sample_count +]); + +/** + * @private {!Uint8Array} + */ +shaka.util.Mp4Generator.VMHD_ = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x01, // flags + 0x00, 0x00, // graphicsmode + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, // opcolor +]); + +/** + * @private {!Uint8Array} + */ +shaka.util.Mp4Generator.SMHD_ = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, // balance, 0 means centered + 0x00, 0x00, // reserved +]); + +/** + * @private {!Uint8Array} + */ +shaka.util.Mp4Generator.STSD_ = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x01, // entry_count +]); + +/** + * @private {!Uint8Array} + */ +shaka.util.Mp4Generator.FTYP_ = new Uint8Array([]); + +/** + * @private {!Uint8Array} + */ +shaka.util.Mp4Generator.DREF_ = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x01, // entry_count + 0x00, 0x00, 0x00, 0x0c, // entry_size + 0x75, 0x72, 0x6c, 0x20, // 'url' type + 0x00, // version 0 + 0x00, 0x00, 0x01, // entry_flags +]); + +/** + * @private {!Uint8Array} + */ +shaka.util.Mp4Generator.DINF_ = new Uint8Array([]);