diff --git a/lib/mss/mss_parser.js b/lib/mss/mss_parser.js index 5fd1a7aea53..de1e5c0f2fd 100644 --- a/lib/mss/mss_parser.js +++ b/lib/mss/mss_parser.js @@ -633,6 +633,8 @@ shaka.mss.MssParser = class { } /** @type {shaka.util.Mp4Generator.StreamInfo} */ const streamInfo = { + id: stream.id, + type: stream.type, encrypted: stream.encrypted, timescale: stream.mssPrivateData.timescale, duration: stream.mssPrivateData.duration, @@ -642,7 +644,7 @@ shaka.mss.MssParser = class { data: null, // Data is not necessary for init segement. stream: stream, }; - const mp4Generator = new shaka.util.Mp4Generator(streamInfo); + const mp4Generator = new shaka.util.Mp4Generator([streamInfo]); initSegmentData = mp4Generator.initSegment(); this.initSegmentDataByStreamId_.set(stream.id, initSegmentData); } diff --git a/lib/transmuxer/aac_transmuxer.js b/lib/transmuxer/aac_transmuxer.js index 30e2164bb29..6161467f6b1 100644 --- a/lib/transmuxer/aac_transmuxer.js +++ b/lib/transmuxer/aac_transmuxer.js @@ -146,7 +146,6 @@ shaka.transmuxer.AacTransmuxer = class { stream.audioSamplingRate = info.sampleRate; stream.channelsCount = info.channelCount; stream.codecs = info.codec; - stream.type = 'audio'; /** @type {!Array.} */ const samples = []; @@ -188,6 +187,8 @@ shaka.transmuxer.AacTransmuxer = class { /** @type {shaka.util.Mp4Generator.StreamInfo} */ const streamInfo = { + id: stream.id, + type: shaka.util.ManifestParserUtils.ContentType.AUDIO, encrypted: stream.encrypted && stream.drmInfos.length > 0, timescale: sampleRate, duration: duration, @@ -201,7 +202,7 @@ shaka.transmuxer.AacTransmuxer = class { }, stream: stream, }; - const mp4Generator = new shaka.util.Mp4Generator(streamInfo); + const mp4Generator = new shaka.util.Mp4Generator([streamInfo]); let initSegment; if (!this.initSegments.has(stream.id)) { initSegment = mp4Generator.initSegment(); diff --git a/lib/transmuxer/ac3_transmuxer.js b/lib/transmuxer/ac3_transmuxer.js index 2728eb020fe..be4bf513b59 100644 --- a/lib/transmuxer/ac3_transmuxer.js +++ b/lib/transmuxer/ac3_transmuxer.js @@ -175,6 +175,8 @@ shaka.transmuxer.Ac3Transmuxer = class { /** @type {shaka.util.Mp4Generator.StreamInfo} */ const streamInfo = { + id: stream.id, + type: shaka.util.ManifestParserUtils.ContentType.AUDIO, encrypted: stream.encrypted && stream.drmInfos.length > 0, timescale: sampleRate, duration: duration, @@ -188,7 +190,7 @@ shaka.transmuxer.Ac3Transmuxer = class { }, stream: stream, }; - const mp4Generator = new shaka.util.Mp4Generator(streamInfo); + const mp4Generator = new shaka.util.Mp4Generator([streamInfo]); let initSegment; if (!this.initSegments.has(stream.id)) { initSegment = mp4Generator.initSegment(); diff --git a/lib/transmuxer/ec3_transmuxer.js b/lib/transmuxer/ec3_transmuxer.js index 8f99bd107ec..168b46fc3cd 100644 --- a/lib/transmuxer/ec3_transmuxer.js +++ b/lib/transmuxer/ec3_transmuxer.js @@ -175,6 +175,8 @@ shaka.transmuxer.Ec3Transmuxer = class { /** @type {shaka.util.Mp4Generator.StreamInfo} */ const streamInfo = { + id: stream.id, + type: shaka.util.ManifestParserUtils.ContentType.AUDIO, encrypted: stream.encrypted && stream.drmInfos.length > 0, timescale: sampleRate, duration: duration, @@ -188,7 +190,7 @@ shaka.transmuxer.Ec3Transmuxer = class { }, stream: stream, }; - const mp4Generator = new shaka.util.Mp4Generator(streamInfo); + const mp4Generator = new shaka.util.Mp4Generator([streamInfo]); let initSegment; if (!this.initSegments.has(stream.id)) { initSegment = mp4Generator.initSegment(); diff --git a/lib/transmuxer/mp3_transmuxer.js b/lib/transmuxer/mp3_transmuxer.js index 913afa08647..9904d033e93 100644 --- a/lib/transmuxer/mp3_transmuxer.js +++ b/lib/transmuxer/mp3_transmuxer.js @@ -167,6 +167,8 @@ shaka.transmuxer.Mp3Transmuxer = class { /** @type {shaka.util.Mp4Generator.StreamInfo} */ const streamInfo = { + id: stream.id, + type: shaka.util.ManifestParserUtils.ContentType.AUDIO, encrypted: stream.encrypted && stream.drmInfos.length > 0, timescale: sampleRate, duration: duration, @@ -180,7 +182,7 @@ shaka.transmuxer.Mp3Transmuxer = class { }, stream: stream, }; - const mp4Generator = new shaka.util.Mp4Generator(streamInfo); + const mp4Generator = new shaka.util.Mp4Generator([streamInfo]); let initSegment; if (!this.initSegments.has(stream.id)) { initSegment = mp4Generator.initSegment(); diff --git a/lib/transmuxer/ts_transmuxer.js b/lib/transmuxer/ts_transmuxer.js index 6963f7fdeea..0a408919921 100644 --- a/lib/transmuxer/ts_transmuxer.js +++ b/lib/transmuxer/ts_transmuxer.js @@ -180,45 +180,59 @@ shaka.transmuxer.TsTransmuxer = class { if (startTime.video != null) { timestamp = startTime.video; } - let streamInfo; + const streamInfos = []; const codecs = tsParser.getCodecs(); try { + let streamInfo = null; + switch (codecs.video) { + case 'avc': + streamInfo = + this.getAvcStreamInfo_(tsParser, timestamp, stream, duration); + break; + } + if (streamInfo) { + streamInfos.push(streamInfo); + streamInfo = null; + } + const isMuxed = !!(codecs.video && codecs.audio); switch (codecs.audio) { case 'aac': streamInfo = - this.getAacStreamInfo_(tsParser, timestamp, stream, duration); + this.getAacStreamInfo_( + tsParser, timestamp, stream, duration, isMuxed); break; case 'ac3': streamInfo = - this.getAc3StreamInfo_(tsParser, timestamp, stream, duration); + this.getAc3StreamInfo_( + tsParser, timestamp, stream, duration, isMuxed); break; case 'ec3': streamInfo = - this.getEc3StreamInfo_(tsParser, timestamp, stream, duration); + this.getEc3StreamInfo_( + tsParser, timestamp, stream, duration, isMuxed); break; case 'mp3': streamInfo = - this.getMp3StreamInfo_(tsParser, timestamp, stream, duration); + this.getMp3StreamInfo_( + tsParser, timestamp, stream, duration, isMuxed); break; } - switch (codecs.video) { - case 'avc': - streamInfo = - this.getAvcStreamInfo_(tsParser, timestamp, stream, duration); - break; + if (streamInfo) { + streamInfos.push(streamInfo); + streamInfo = null; } } catch (e) { return Promise.reject(e); } - if (!streamInfo) { + if (!streamInfos.length) { return Promise.reject(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MEDIA, shaka.util.Error.Code.TRANSMUXING_FAILED)); } - const mp4Generator = new shaka.util.Mp4Generator(streamInfo); + const mp4Generator = new shaka.util.Mp4Generator(streamInfos); let initSegment; if (!this.initSegments.has(stream.id)) { initSegment = mp4Generator.initSegment(); @@ -239,10 +253,11 @@ shaka.transmuxer.TsTransmuxer = class { * @param {number} timestamp * @param {shaka.extern.Stream} stream * @param {number} duration + * @param {boolean} isMuxed * @return {shaka.util.Mp4Generator.StreamInfo} * @private */ - getAacStreamInfo_(tsParser, timestamp, stream, duration) { + getAacStreamInfo_(tsParser, timestamp, stream, duration, isMuxed) { const ADTS = shaka.transmuxer.ADTS; /** @type {!Array.} */ @@ -266,7 +281,6 @@ shaka.transmuxer.TsTransmuxer = class { stream.audioSamplingRate = info.sampleRate; stream.channelsCount = info.channelCount; stream.codecs = info.codec; - stream.type = 'audio'; while (offset < data.length) { const header = ADTS.parseHeader(data, offset); @@ -311,7 +325,13 @@ shaka.transmuxer.TsTransmuxer = class { /** @type {number} */ const baseMediaDecodeTime = Math.floor(timestamp * sampleRate / 1000); + let id = stream.id; + if (isMuxed) { + id += shaka.transmuxer.TsTransmuxer.START_ID_FOR_MUXED_CONTENT_; + } return { + id: id, + type: shaka.util.ManifestParserUtils.ContentType.AUDIO, encrypted: stream.encrypted && stream.drmInfos.length > 0, timescale: sampleRate, duration: duration, @@ -333,10 +353,11 @@ shaka.transmuxer.TsTransmuxer = class { * @param {number} timestamp * @param {shaka.extern.Stream} stream * @param {number} duration + * @param {boolean} isMuxed * @return {shaka.util.Mp4Generator.StreamInfo} * @private */ - getAc3StreamInfo_(tsParser, timestamp, stream, duration) { + getAc3StreamInfo_(tsParser, timestamp, stream, duration, isMuxed) { const Ac3 = shaka.transmuxer.Ac3; /** @type {!Array.} */ @@ -394,7 +415,13 @@ shaka.transmuxer.TsTransmuxer = class { /** @type {number} */ const baseMediaDecodeTime = Math.floor(timestamp * sampleRate / 1000); + let id = stream.id; + if (isMuxed) { + id += shaka.transmuxer.TsTransmuxer.START_ID_FOR_MUXED_CONTENT_; + } return { + id: id, + type: shaka.util.ManifestParserUtils.ContentType.AUDIO, encrypted: stream.encrypted && stream.drmInfos.length > 0, timescale: sampleRate, duration: duration, @@ -416,10 +443,11 @@ shaka.transmuxer.TsTransmuxer = class { * @param {number} timestamp * @param {shaka.extern.Stream} stream * @param {number} duration + * @param {boolean} isMuxed * @return {shaka.util.Mp4Generator.StreamInfo} * @private */ - getEc3StreamInfo_(tsParser, timestamp, stream, duration) { + getEc3StreamInfo_(tsParser, timestamp, stream, duration, isMuxed) { const Ec3 = shaka.transmuxer.Ec3; /** @type {!Array.} */ @@ -477,7 +505,13 @@ shaka.transmuxer.TsTransmuxer = class { /** @type {number} */ const baseMediaDecodeTime = Math.floor(timestamp * sampleRate / 1000); + let id = stream.id; + if (isMuxed) { + id += shaka.transmuxer.TsTransmuxer.START_ID_FOR_MUXED_CONTENT_; + } return { + id: id, + type: shaka.util.ManifestParserUtils.ContentType.AUDIO, encrypted: stream.encrypted && stream.drmInfos.length > 0, timescale: sampleRate, duration: duration, @@ -499,10 +533,11 @@ shaka.transmuxer.TsTransmuxer = class { * @param {number} timestamp * @param {shaka.extern.Stream} stream * @param {number} duration + * @param {boolean} isMuxed * @return {shaka.util.Mp4Generator.StreamInfo} * @private */ - getMp3StreamInfo_(tsParser, timestamp, stream, duration) { + getMp3StreamInfo_(tsParser, timestamp, stream, duration, isMuxed) { const MpegAudio = shaka.transmuxer.MpegAudio; /** @type {!Array.} */ @@ -555,7 +590,13 @@ shaka.transmuxer.TsTransmuxer = class { /** @type {number} */ const baseMediaDecodeTime = Math.floor(timestamp * sampleRate / 1000); + let id = stream.id; + if (isMuxed) { + id += shaka.transmuxer.TsTransmuxer.START_ID_FOR_MUXED_CONTENT_; + } return { + id: id, + type: shaka.util.ManifestParserUtils.ContentType.AUDIO, encrypted: stream.encrypted && stream.drmInfos.length > 0, timescale: sampleRate, duration: duration, @@ -643,6 +684,8 @@ shaka.transmuxer.TsTransmuxer = class { stream.width = info.width; return { + id: stream.id, + type: shaka.util.ManifestParserUtils.ContentType.VIDEO, encrypted: stream.encrypted && stream.drmInfos.length > 0, timescale: timescale, duration: duration, @@ -682,6 +725,12 @@ shaka.transmuxer.TsTransmuxer.SUPPORTED_VIDEO_CODECS_ = [ 'avc', ]; +/** + * @private + * @const {number} + */ +shaka.transmuxer.TsTransmuxer.START_ID_FOR_MUXED_CONTENT_ = 128; + shaka.transmuxer.TransmuxerEngine.registerTransmuxer( 'video/mp2t', diff --git a/lib/util/mp4_generator.js b/lib/util/mp4_generator.js index 99ac0a33a99..420d24412b9 100644 --- a/lib/util/mp4_generator.js +++ b/lib/util/mp4_generator.js @@ -13,50 +13,13 @@ goog.require('shaka.util.Uint8ArrayUtils'); shaka.util.Mp4Generator = class { /** - * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo + * @param {!Array.} streamInfos */ - constructor(streamInfo) { + constructor(streamInfos) { shaka.util.Mp4Generator.initStaticProperties_(); - /** @private {!shaka.extern.Stream} */ - this.stream_ = streamInfo.stream; - - /** @private {boolean} */ - this.encrypted_ = streamInfo.encrypted; - - /** @private {number} */ - this.timescale_ = streamInfo.timescale; - - /** @private {number} */ - this.duration_ = streamInfo.duration; - if (this.duration_ === Infinity) { - this.duration_ = 0xffffffff; - } - - /** @private {!Array.} */ - this.videoNalus_ = streamInfo.videoNalus; - - /** @private {!Uint8Array} */ - this.audioConfig_ = streamInfo.audioConfig; - - /** @private {!Uint8Array} */ - this.videoConfig_ = streamInfo.videoConfig; - - /** @private {number} */ - this.sequenceNumber_ = 0; - - /** @private {number} */ - this.baseMediaDecodeTime_ = 0; - - /** @private {!Array.} */ - this.samples_ = []; - - const data = streamInfo.data; - if (data) { - this.sequenceNumber_ = data.sequenceNumber; - this.baseMediaDecodeTime_ = data.baseMediaDecodeTime; - this.samples_ = data.samples; - } + /** @private {!Array.} */ + this.streamInfos_ = streamInfos; } /** @@ -81,20 +44,31 @@ shaka.util.Mp4Generator = class { * @private */ moov_() { + goog.asserts.assert(this.streamInfos_.length > 0, + 'StreamInfos must have elements'); const Mp4Generator = shaka.util.Mp4Generator; + let traks = new Uint8Array([]); + for (const streamInfo of this.streamInfos_) { + traks = shaka.util.Uint8ArrayUtils.concat(traks, this.trak_(streamInfo)); + } + const firstStreamInfo = this.streamInfos_[0]; return Mp4Generator.box('moov', - this.mvhd_(), this.trak_(), this.mvex_(), this.pssh_()); + this.mvhd_(firstStreamInfo), + traks, + this.mvex_(firstStreamInfo), + this.pssh_(firstStreamInfo)); } /** * Generate a MVHD box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - mvhd_() { + mvhd_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; - const duration = this.duration_ * this.timescale_; + const duration = streamInfo.duration * streamInfo.timescale; const upperWordDuration = Math.floor(duration / (Mp4Generator.UINT32_MAX_ + 1)); const lowerWordDuration = @@ -106,7 +80,7 @@ shaka.util.Mp4Generator = class { 0x00, 0x00, 0x00, 0x02, // creation_time 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, // modification_time - ...this.breakNumberIntoBytes_(this.timescale_, 4), // timescale + ...this.breakNumberIntoBytes_(streamInfo.timescale, 4), // timescale ...this.breakNumberIntoBytes_(upperWordDuration, 4), ...this.breakNumberIntoBytes_(lowerWordDuration, 4), // duration 0x00, 0x01, 0x00, 0x00, // 1.0 rate @@ -137,26 +111,29 @@ shaka.util.Mp4Generator = class { /** * Generate a TRAK box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - trak_() { + trak_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; - return Mp4Generator.box('trak', this.tkhd_(), this.mdia_()); + return Mp4Generator.box('trak', + this.tkhd_(streamInfo), this.mdia_(streamInfo)); } /** * Generate a TKHD box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - tkhd_() { + tkhd_(streamInfo) { 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 id = streamInfo.id + 1; + const width = streamInfo.stream.width || 0; + const height = streamInfo.stream.height || 0; + const duration = streamInfo.duration * streamInfo.timescale; const upperWordDuration = Math.floor(duration / (Mp4Generator.UINT32_MAX_ + 1)); const lowerWordDuration = @@ -198,29 +175,31 @@ shaka.util.Mp4Generator = class { /** * Generate a MDIA box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - mdia_() { + mdia_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; - return Mp4Generator.box( - 'mdia', this.mdhd_(), this.hdlr_(), this.minf_()); + return Mp4Generator.box( 'mdia', this.mdhd_(streamInfo), + this.hdlr_(streamInfo), this.minf_(streamInfo)); } /** * Generate a MDHD box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - mdhd_() { + mdhd_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; - const duration = this.duration_ * this.timescale_; + const duration = streamInfo.duration * streamInfo.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 language = streamInfo.stream.language; const languageNumber = ((language.charCodeAt(0) - 0x60) << 10) | ((language.charCodeAt(1) - 0x60) << 5) | ((language.charCodeAt(2) - 0x60)); @@ -231,7 +210,7 @@ shaka.util.Mp4Generator = class { 0x00, 0x00, 0x00, 0x02, // creation_time 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, // modification_time - ...this.breakNumberIntoBytes_(this.timescale_, 4), // timescale + ...this.breakNumberIntoBytes_(streamInfo.timescale, 4), // timescale ...this.breakNumberIntoBytes_(upperWordDuration, 4), ...this.breakNumberIntoBytes_(lowerWordDuration, 4), // duration ...this.breakNumberIntoBytes_(languageNumber, 2), // language @@ -243,14 +222,15 @@ shaka.util.Mp4Generator = class { /** * Generate a HDLR box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - hdlr_() { + hdlr_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; const ContentType = shaka.util.ManifestParserUtils.ContentType; let bytes = new Uint8Array([]); - switch (this.stream_.type) { + switch (streamInfo.type) { case ContentType.VIDEO: bytes = Mp4Generator.HDLR_TYPES_.video; break; @@ -264,21 +244,22 @@ shaka.util.Mp4Generator = class { /** * Generate a MINF box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - minf_() { + minf_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; const ContentType = shaka.util.ManifestParserUtils.ContentType; - switch (this.stream_.type) { + switch (streamInfo.type) { case ContentType.VIDEO: return Mp4Generator.box( 'minf', Mp4Generator.box('vmhd', Mp4Generator.VMHD_), - Mp4Generator.DINF_, this.stbl_()); + Mp4Generator.DINF_, this.stbl_(streamInfo)); case ContentType.AUDIO: return Mp4Generator.box( 'minf', Mp4Generator.box('smhd', Mp4Generator.SMHD_), - Mp4Generator.DINF_, this.stbl_()); + Mp4Generator.DINF_, this.stbl_(streamInfo)); } return new Uint8Array([]); } @@ -286,14 +267,15 @@ shaka.util.Mp4Generator = class { /** * Generate a STBL box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - stbl_() { + stbl_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; return Mp4Generator.box( 'stbl', - this.stsd_(), + this.stsd_(streamInfo), Mp4Generator.box('stts', Mp4Generator.STTS_), Mp4Generator.box('stsc', Mp4Generator.STSC_), Mp4Generator.box('stsz', Mp4Generator.STSZ_), @@ -303,30 +285,31 @@ shaka.util.Mp4Generator = class { /** * Generate a STSD box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - stsd_() { + stsd_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; const ContentType = shaka.util.ManifestParserUtils.ContentType; let bytes = new Uint8Array([]); - switch (this.stream_.type) { + switch (streamInfo.type) { case ContentType.VIDEO: - bytes = this.avc1_(); + bytes = this.avc1_(streamInfo); 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 if (this.stream_.mimeType === 'audio/ac3' || - this.stream_.codecs.includes('ac-3')) { - bytes = this.ac3_(); - } else if (this.stream_.mimeType === 'audio/ec3' || - this.stream_.codecs.includes('ec-3')) { - bytes = this.ec3_(); + if (streamInfo.stream.mimeType === 'audio/mpeg' || + streamInfo.stream.codecs.includes('mp3') || + streamInfo.stream.codecs.includes('mp4a.40.34')) { + bytes = this.mp3_(streamInfo); + } else if (streamInfo.stream.mimeType === 'audio/ac3' || + streamInfo.stream.codecs.includes('ac-3')) { + bytes = this.ac3_(streamInfo); + } else if (streamInfo.stream.mimeType === 'audio/ec3' || + streamInfo.stream.codecs.includes('ec-3')) { + bytes = this.ec3_(streamInfo); } else { - bytes = this.mp4a_(); + bytes = this.mp4a_(streamInfo); } break; } @@ -336,20 +319,21 @@ shaka.util.Mp4Generator = class { /** * Generate a AVC1 box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - avc1_() { + avc1_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; - const width = this.stream_.width || 0; - const height = this.stream_.height || 0; + const width = streamInfo.stream.width || 0; + const height = streamInfo.stream.height || 0; let avcCBox; - if (this.videoConfig_.byteLength > 0) { - avcCBox = Mp4Generator.box('avcC', this.videoConfig_); + if (streamInfo.videoConfig.byteLength > 0) { + avcCBox = Mp4Generator.box('avcC', streamInfo.videoConfig); } else { - avcCBox = Mp4Generator.box('avcC', this.avcC_()); + avcCBox = Mp4Generator.box('avcC', this.avcC_(streamInfo)); } const avc1Bytes = new Uint8Array([ @@ -380,13 +364,10 @@ shaka.util.Mp4Generator = class { 0x11, 0x11, // pre_defined = -1 ]); - let sinfBox = new Uint8Array([]); - if (this.encrypted_) { - sinfBox = this.sinf_(); - } - let boxName = 'avc1'; - if (this.encrypted_) { + let sinfBox = new Uint8Array([]); + if (streamInfo.encrypted) { + sinfBox = this.sinf_(streamInfo); boxName = 'encv'; } return Mp4Generator.box(boxName, avc1Bytes, avcCBox, sinfBox); @@ -395,10 +376,11 @@ shaka.util.Mp4Generator = class { /** * Generate a AVCC box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - avcC_() { + avcC_(streamInfo) { const NALUTYPE_SPS = 7; const NALUTYPE_PPS = 8; @@ -411,8 +393,8 @@ shaka.util.Mp4Generator = class { 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]); + for (let i = 0; i < streamInfo.videoNalus.length; i++) { + const naluBytes = this.hexStringToBuffer_(streamInfo.videoNalus[i]); const naluType = naluBytes[0] & 0x1F; switch (naluType) { case NALUTYPE_SPS: @@ -468,12 +450,13 @@ shaka.util.Mp4Generator = class { /** * Generate STSD bytes * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - audioStsd_() { - const channelsCount = this.stream_.channelsCount || 2; - const audioSamplingRate = this.stream_.audioSamplingRate || 44100; + audioStsd_(streamInfo) { + const channelsCount = streamInfo.stream.channelsCount || 2; + const audioSamplingRate = streamInfo.stream.audioSamplingRate || 44100; const bytes = new Uint8Array([ 0x00, 0x00, 0x00, // reserved 0x00, 0x00, 0x00, // reserved @@ -493,101 +476,100 @@ shaka.util.Mp4Generator = class { /** * Generate a .MP3 box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - mp3_() { + mp3_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; - return Mp4Generator.box('.mp3', this.audioStsd_()); + return Mp4Generator.box('.mp3', this.audioStsd_(streamInfo)); } /** * Generate a AC-3 box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - ac3_() { + ac3_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; - const dac3Box = Mp4Generator.box('dac3', this.audioConfig_); - - let sinfBox = new Uint8Array([]); - if (this.encrypted_) { - sinfBox = this.sinf_(); - } + const dac3Box = Mp4Generator.box('dac3', streamInfo.audioConfig); let boxName = 'ac-3'; - if (this.encrypted_) { + let sinfBox = new Uint8Array([]); + if (streamInfo.encrypted) { + sinfBox = this.sinf_(streamInfo); boxName = 'enca'; } - return Mp4Generator.box(boxName, this.audioStsd_(), dac3Box, sinfBox); + return Mp4Generator.box(boxName, + this.audioStsd_(streamInfo), dac3Box, sinfBox); } /** * Generate a EC-3 box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - ec3_() { + ec3_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; - const dec3Box = Mp4Generator.box('dec3', this.audioConfig_); - - let sinfBox = new Uint8Array([]); - if (this.encrypted_) { - sinfBox = this.sinf_(); - } + const dec3Box = Mp4Generator.box('dec3', streamInfo.audioConfig); let boxName = 'ec-3'; - if (this.encrypted_) { + let sinfBox = new Uint8Array([]); + if (streamInfo.encrypted) { + sinfBox = this.sinf_(streamInfo); boxName = 'enca'; } - return Mp4Generator.box(boxName, this.audioStsd_(), dec3Box, sinfBox); + return Mp4Generator.box(boxName, + this.audioStsd_(streamInfo), dec3Box, sinfBox); } /** * Generate a MP4A box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - mp4a_() { + mp4a_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; let esdsBox; - if (this.audioConfig_.byteLength > 0) { - esdsBox = Mp4Generator.box('esds', this.audioConfig_); + if (streamInfo.audioConfig.byteLength > 0) { + esdsBox = Mp4Generator.box('esds', streamInfo.audioConfig); } else { - esdsBox = Mp4Generator.box('esds', this.esds_()); - } - - let sinfBox = new Uint8Array([]); - if (this.encrypted_) { - sinfBox = this.sinf_(); + esdsBox = Mp4Generator.box('esds', this.esds_(streamInfo)); } let boxName = 'mp4a'; - if (this.encrypted_) { + let sinfBox = new Uint8Array([]); + if (streamInfo.encrypted) { + sinfBox = this.sinf_(streamInfo); boxName = 'enca'; } - return Mp4Generator.box(boxName, this.audioStsd_(), esdsBox, sinfBox); + return Mp4Generator.box(boxName, + this.audioStsd_(streamInfo), esdsBox, sinfBox); } /** * Generate a ESDS box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - esds_() { + esds_(streamInfo) { 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 id = streamInfo.id + 1; + const bandwidth = streamInfo.stream.bandwidth || 0; + const channelsCount = streamInfo.stream.channelsCount || 2; + const audioSamplingRate = streamInfo.stream.audioSamplingRate || 44100; const audioCodec = shaka.util.ManifestParserUtils.guessCodecs( - ContentType.AUDIO, this.stream_.codecs.split(',')); + ContentType.AUDIO, streamInfo.stream.codecs.split(',')); const samplingFrequencyIndex = { 96000: 0x0, @@ -646,23 +628,25 @@ shaka.util.Mp4Generator = class { /** * Generate a MVEX box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - mvex_() { + mvex_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; - return Mp4Generator.box('mvex', this.trex_()); + return Mp4Generator.box('mvex', this.trex_(streamInfo)); } /** * Generate a TREX box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - trex_() { + trex_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; - const id = this.stream_.id + 1; + const id = streamInfo.id + 1; const bytes = new Uint8Array([ 0x00, // version 0 0x00, 0x00, 0x00, // flags @@ -678,16 +662,17 @@ shaka.util.Mp4Generator = class { /** * Generate a PSSH box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - pssh_() { + pssh_(streamInfo) { let boxes = new Uint8Array([]); - if (!this.encrypted_) { + if (!streamInfo.encrypted) { return boxes; } - for (const drmInfo of this.stream_.drmInfos) { + for (const drmInfo of streamInfo.stream.drmInfos) { if (!drmInfo.initData) { continue; } @@ -701,24 +686,26 @@ shaka.util.Mp4Generator = class { /** * Generate a SINF box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - sinf_() { + sinf_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; return Mp4Generator.box('sinf', - this.frma_(), this.schm_(), this.schi_()); + this.frma_(streamInfo), this.schm_(), this.schi_(streamInfo)); } /** * Generate a FRMA box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - frma_() { - const codec = this.stream_.codecs.substring( - 0, this.stream_.codecs.indexOf('.')); + frma_(streamInfo) { + const codec = streamInfo.stream.codecs.substring( + 0, streamInfo.stream.codecs.indexOf('.')); const Mp4Generator = shaka.util.Mp4Generator; const codecNumber = this.stringToCharCode_(codec); const bytes = new Uint8Array([ @@ -747,27 +734,29 @@ shaka.util.Mp4Generator = class { /** * Generate a SCHI box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - schi_() { + schi_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; - return Mp4Generator.box('schi', this.tenc_()); + return Mp4Generator.box('schi', this.tenc_(streamInfo)); } /** * Generate a TENC box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - tenc_() { + tenc_(streamInfo) { // 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) { + for (const drmInfo of streamInfo.stream.drmInfos) { if (drmInfo && drmInfo.keyIds && drmInfo.keyIds.size) { for (const keyId of drmInfo.keyIds) { defaultKeyId = this.hexStringToBuffer_(keyId); @@ -793,38 +782,42 @@ shaka.util.Mp4Generator = class { * @return {!Uint8Array} */ segmentData() { - const movie = this.moof_(); - const mdat = this.mdat_(); - const length = movie.byteLength + mdat.byteLength; - const result = new Uint8Array(length); - result.set(movie); - result.set(mdat, movie.byteLength); + let result = new Uint8Array([]); + for (const streamInfo of this.streamInfos_) { + result = shaka.util.Uint8ArrayUtils.concat(result, + this.moof_(streamInfo), this.mdat_(streamInfo)); + } return result; } /** * Generate a MOOF box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - moof_() { + moof_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; - return Mp4Generator.box('moof', this.mfhd_(), this.traf_()); + return Mp4Generator.box('moof', + this.mfhd_(streamInfo), this.traf_(streamInfo)); } /** * Generate a MOOF box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - mfhd_() { + mfhd_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; + const sequenceNumber = + streamInfo.data ? streamInfo.data.sequenceNumber : 0; const bytes = new Uint8Array([ 0x00, // version 0 0x00, 0x00, 0x00, // flags - ...this.breakNumberIntoBytes_(this.sequenceNumber_, 4), + ...this.breakNumberIntoBytes_(sequenceNumber, 4), ]); return Mp4Generator.box('mfhd', bytes); } @@ -832,12 +825,13 @@ shaka.util.Mp4Generator = class { /** * Generate a TRAF box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - traf_() { + traf_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; - const sampleDependencyTable = this.sdtp_(); + const sampleDependencyTable = this.sdtp_(streamInfo); const offset = sampleDependencyTable.length + 32 + // tfhd 20 + // tfdt @@ -845,23 +839,28 @@ shaka.util.Mp4Generator = class { 16 + // mfhd 8 + // moof header 8; // mdat header; - return Mp4Generator.box('traf', this.tfhd_(), this.tfdt_(), - this.trun_(offset), sampleDependencyTable); + return Mp4Generator.box('traf', + this.tfhd_(streamInfo), + this.tfdt_(streamInfo), + this.trun_(streamInfo, offset), + sampleDependencyTable); } /** * Generate a SDTP box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - sdtp_() { + sdtp_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; - const bytes = new Uint8Array(4 + this.samples_.length); + const samples = streamInfo.data ? streamInfo.data.samples : []; + const bytes = new Uint8Array(4 + samples.length); // leave the full box header (4 bytes) all zero // write the sample table - for (let i = 0; i < this.samples_.length; i++) { - const flags = this.samples_[i].flags; + for (let i = 0; i < samples.length; i++) { + const flags = samples[i].flags; bytes[i + 4] = (flags.dependsOn << 4) | (flags.isDependedOn << 2) | flags.hasRedundancy; @@ -872,12 +871,13 @@ shaka.util.Mp4Generator = class { /** * Generate a TFHD box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - tfhd_() { + tfhd_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; - const id = this.stream_.id + 1; + const id = streamInfo.id + 1; const bytes = new Uint8Array([ 0x00, // version 0 0x00, 0x00, 0x3a, // flags @@ -893,15 +893,18 @@ shaka.util.Mp4Generator = class { /** * Generate a TFDT box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo * @return {!Uint8Array} * @private */ - tfdt_() { + tfdt_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; + const baseMediaDecodeTime = + streamInfo.data ? streamInfo.data.baseMediaDecodeTime : 0; const upperWordBaseMediaDecodeTime = - Math.floor(this.baseMediaDecodeTime_ / (Mp4Generator.UINT32_MAX_ + 1)); + Math.floor(baseMediaDecodeTime / (Mp4Generator.UINT32_MAX_ + 1)); const lowerWordBaseMediaDecodeTime = - Math.floor(this.baseMediaDecodeTime_ % (Mp4Generator.UINT32_MAX_ + 1)); + Math.floor(baseMediaDecodeTime % (Mp4Generator.UINT32_MAX_ + 1)); const bytes = new Uint8Array([ 0x01, // version 1 0x00, 0x00, 0x00, // flags @@ -914,18 +917,21 @@ shaka.util.Mp4Generator = class { /** * Generate a TRUN box * + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo + * @param {number} offset * @return {!Uint8Array} * @private */ - trun_(offset) { + trun_(streamInfo, offset) { const ContentType = shaka.util.ManifestParserUtils.ContentType; const Mp4Generator = shaka.util.Mp4Generator; - const samplesLength = this.samples_.length; + const samples = streamInfo.data ? streamInfo.data.samples : []; + const samplesLength = samples.length; const byteslen = 12 + 16 * samplesLength; const bytes = new Uint8Array(byteslen); offset += 8 + byteslen; - const isVideo = this.stream_.type === ContentType.VIDEO; + const isVideo = streamInfo.type === ContentType.VIDEO; bytes.set( [ // version 1 for video with signed-int sample_composition_time_offset @@ -937,7 +943,7 @@ shaka.util.Mp4Generator = class { 0, ); for (let i = 0; i < samplesLength; i++) { - const sample = this.samples_[i]; + const sample = samples[i]; const duration = this.breakNumberIntoBytes_(sample.duration, 4); const size = this.breakNumberIntoBytes_(sample.size, 4); const flags = sample.flags; @@ -965,10 +971,11 @@ shaka.util.Mp4Generator = class { * @return {!Uint8Array} * @private */ - mdat_() { + mdat_(streamInfo) { const Mp4Generator = shaka.util.Mp4Generator; + const samples = streamInfo.data ? streamInfo.data.samples : []; let bytes = new Uint8Array(0); - for (const sample of this.samples_) { + for (const sample of samples) { bytes = shaka.util.Uint8ArrayUtils.concat(bytes, sample.data); } return Mp4Generator.box('mdat', bytes); @@ -1220,6 +1227,8 @@ shaka.util.Mp4Generator.DINF_ = new Uint8Array([]); /** * @typedef {{ + * id: number, + * type: string, * encrypted: boolean, * timescale: number, * duration: number, @@ -1230,6 +1239,10 @@ shaka.util.Mp4Generator.DINF_ = new Uint8Array([]); * stream: !shaka.extern.Stream * }} * + * @property {number} id + * A unique ID + * @property {string} type + * Indicate the content type: 'video' or'audio'. * @property {boolean} encrypted * Indicate if the stream is encrypted. * @property {number} timescale