diff --git a/lib/mss/mss_parser.js b/lib/mss/mss_parser.js index e8e4ab3e36..9e5c42d363 100644 --- a/lib/mss/mss_parser.js +++ b/lib/mss/mss_parser.js @@ -618,15 +618,20 @@ shaka.mss.MssParser = class { if (this.initSegmentDataByStreamId_.has(stream.id)) { initSegmentData = this.initSegmentDataByStreamId_.get(stream.id); } else { - const timescale = stream.mssPrivateData.timescale; - const duration = stream.mssPrivateData.duration; - let videoNalus = null; + let videoNalus = []; 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); + /** @type {shaka.util.Mp4Generator.StreamInfo} */ + const streamInfo = { + timescale: stream.mssPrivateData.timescale, + duration: stream.mssPrivateData.duration, + videoNalus: videoNalus, + data: null, // Data is not necessary for init segement. + stream: stream, + }; + const mp4Generator = new shaka.util.Mp4Generator(streamInfo); initSegmentData = mp4Generator.initSegment(); this.initSegmentDataByStreamId_.set(stream.id, initSegmentData); } diff --git a/lib/util/mp4_generator.js b/lib/util/mp4_generator.js index 5092284dae..69dc8c4b85 100644 --- a/lib/util/mp4_generator.js +++ b/lib/util/mp4_generator.js @@ -11,33 +11,43 @@ 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 + * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo */ - constructor(stream, timescale, duration, videoNalus) { + constructor(streamInfo) { shaka.util.Mp4Generator.initStaticProperties_(); - /** @private {shaka.extern.Stream} */ - this.stream_ = stream; + /** @private {!shaka.extern.Stream} */ + this.stream_ = streamInfo.stream; /** @private {number} */ - this.timescale_ = timescale; + this.timescale_ = streamInfo.timescale; /** @private {number} */ - this.duration_ = duration || 0xffffffff; + this.duration_ = streamInfo.duration; if (this.duration_ === Infinity) { this.duration_ = 0xffffffff; } - /** @private {Array.} */ - this.videoNalus_ = videoNalus || []; + /** @private {!Array.} */ + this.videoNalus_ = streamInfo.videoNalus; + + /** @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; + } } /** @@ -698,6 +708,179 @@ shaka.util.Mp4Generator = class { return Mp4Generator.box('tenc', bytes, defaultKeyId); } + /** + * Generate a Segment Data (MP4). + * + * @return {!Uint8Array} + */ + segmentData() { + const Mp4Generator = shaka.util.Mp4Generator; + const movie = this.moof_(); + 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 MOOF box + * + * @return {!Uint8Array} + * @private + */ + moof_() { + const Mp4Generator = shaka.util.Mp4Generator; + return Mp4Generator.box('moof', this.mfhd_(), this.traf_()); + } + + /** + * Generate a MOOF box + * + * @return {!Uint8Array} + * @private + */ + mfhd_() { + const Mp4Generator = shaka.util.Mp4Generator; + const bytes = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + ...this.breakNumberIntoBytes_(this.sequenceNumber_, 4), + ]); + return Mp4Generator.box('mfhd', bytes); + } + + /** + * Generate a TRAF box + * + * @return {!Uint8Array} + * @private + */ + traf_() { + const Mp4Generator = shaka.util.Mp4Generator; + const sampleDependencyTable = this.sdtp_(); + const offset = sampleDependencyTable.length + + 32 + // tfhd + 20 + // tfdt + 8 + // traf header + 16 + // mfhd + 8 + // moof header + 8; // mdat header; + return Mp4Generator.box('traf', this.tfhd_(), this.tfdt_(), + this.trun_(offset), sampleDependencyTable); + } + + /** + * Generate a SDTP box + * + * @return {!Uint8Array} + * @private + */ + sdtp_() { + const Mp4Generator = shaka.util.Mp4Generator; + const bytes = new Uint8Array(4 + this.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; + bytes[i + 4] = (flags.dependsOn << 4) | + (flags.isDependedOn << 2) | + flags.hasRedundancy; + } + return Mp4Generator.box('sdtp', bytes); + } + + /** + * Generate a TFHD box + * + * @return {!Uint8Array} + * @private + */ + tfhd_() { + const Mp4Generator = shaka.util.Mp4Generator; + const id = this.stream_.id + 1; + const bytes = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x3a, // flags + ...this.breakNumberIntoBytes_(id, 4), // track_ID + 0x00, 0x00, 0x00, 0x01, // sample_description_index + 0x00, 0x00, 0x00, 0x00, // default_sample_duration + 0x00, 0x00, 0x00, 0x00, // default_sample_size + 0x00, 0x00, 0x00, 0x00, // default_sample_flags + ]); + return Mp4Generator.box('tfhd', bytes); + } + + /** + * Generate a TFDT box + * + * @return {!Uint8Array} + * @private + */ + tfdt_() { + const Mp4Generator = shaka.util.Mp4Generator; + const upperWordBaseMediaDecodeTime = + Math.floor(this.baseMediaDecodeTime_ / (Mp4Generator.UINT32_MAX_ + 1)); + const lowerWordBaseMediaDecodeTime = + Math.floor(this.baseMediaDecodeTime_ % (Mp4Generator.UINT32_MAX_ + 1)); + const bytes = new Uint8Array([ + 0x01, // version 1 + 0x00, 0x00, 0x00, // flags + ...this.breakNumberIntoBytes_(upperWordBaseMediaDecodeTime, 4), + ...this.breakNumberIntoBytes_(lowerWordBaseMediaDecodeTime, 4), + ]); + return Mp4Generator.box('mfhd', bytes); + } + + /** + * Generate a TRUN box + * + * @return {!Uint8Array} + * @private + */ + trun_(offset) { + const ContentType = shaka.util.ManifestParserUtils.ContentType; + const Mp4Generator = shaka.util.Mp4Generator; + + const samplesLength = this.samples_.length; + const byteslen = 12 + 16 * samplesLength; + const bytes = new Uint8Array(byteslen); + offset += 8 + byteslen; + const isVideo = this.stream_.type === ContentType.VIDEO; + bytes.set( + [ + // version 1 for video with signed-int sample_composition_time_offset + isVideo ? 0x01 : 0x00, + 0x00, 0x0f, 0x01, // flags + ...this.breakNumberIntoBytes_(samplesLength, 4), // sample_count + ...this.breakNumberIntoBytes_(offset, 4), // data_offset + ], + 0, + ); + for (let i = 0; i < samplesLength; i++) { + const sample = this.samples_[i]; + const duration = this.breakNumberIntoBytes_(sample.duration, 4); + const size = this.breakNumberIntoBytes_(sample.size, 4); + const flags = sample.flags; + const cts = this.breakNumberIntoBytes_(sample.cts, 4); + bytes.set( + [ + ...duration, // sample_duration + ...size, // sample_size + (flags.isLeading << 2) | flags.dependsOn, + (flags.isDependedOn << 6) | (flags.hasRedundancy << 4) | + flags.isNonSync, + flags.degradPrio & (0xf0 << 8), + flags.degradPrio & 0x0f, // sample_flags + ...cts, // sample_composition_time_offset + ], + 12 + 16 * i, + ); + } + return Mp4Generator.box('trun', bytes); + } + + /** * @param {number} number * @param {number} numBytes @@ -940,3 +1123,79 @@ shaka.util.Mp4Generator.DREF_ = new Uint8Array([ * @private {!Uint8Array} */ shaka.util.Mp4Generator.DINF_ = new Uint8Array([]); + +/** + * @typedef {{ + * timescale: number, + * duration: number, + * videoNalus: !Array., + * data: ?shaka.util.Mp4Generator.Data, + * stream: !shaka.extern.Stream + * }} + * + * @property {number} timescale + * The Stream's timescale. + * @property {number} duration + * The Stream's duration. + * @property {!Array.} videoNalus + * The stream's video nalus. + * @property {?shaka.util.Mp4Generator.Data} data + * The stream's data. + * @property {!shaka.extern.Stream} stream + * The Stream. + */ +shaka.util.Mp4Generator.StreamInfo; + +/** + * @typedef {{ + * sequenceNumber: number, + * baseMediaDecodeTime: number, + * samples: !Array. + * }} + * + * @property {number} sequenceNumber + * The sequence number. + * @property {number} baseMediaDecodeTime + * The base media decode time. + * @property {!Array.} samples + * The data samples. + */ +shaka.util.Mp4Generator.Data; + +/** + * @typedef {{ + * size: number, + * duration: number, + * cts: number, + * flags: !shaka.util.Mp4Generator.Mp4SampleFlags + * }} + * + * @property {number} size + * The sample size. + * @property {number} duration + * The sample duration. + * @property {number} cts + * The sample composition time. + * @property {!shaka.util.Mp4Generator.Mp4SampleFlags} flags + * The sample flags. + */ +shaka.util.Mp4Generator.Mp4Sample; + +/** + * @typedef {{ + * isLeading: number, + * isDependedOn: number, + * hasRedundancy: number, + * degradPrio: number, + * dependsOn: number, + * isNonSync: number + * }} + * + * @property {number} isLeading + * @property {number} isDependedOn + * @property {number} hasRedundancy + * @property {number} degradPrio + * @property {number} dependsOn + * @property {number} isNonSync + */ +shaka.util.Mp4Generator.Mp4SampleFlags;