Source: lib/util/mp4_generator.js

/*! @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');


shaka.util.Mp4Generator = class {
  /**
   * @param {!Array.<shaka.util.Mp4Generator.StreamInfo>} streamInfos
   */
  constructor(streamInfos) {
    shaka.util.Mp4Generator.initStaticProperties_();

    /** @private {!Array.<shaka.util.Mp4Generator.StreamInfo>} */
    this.streamInfos_ = streamInfos;
  }

  /**
   * 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_() {
    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_(firstStreamInfo),
        traks,
        this.mvex_(),
        this.pssh_(firstStreamInfo));
  }

  /**
   * Generate a MVHD box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  mvhd_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    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 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_(streamInfo.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
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  trak_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    return Mp4Generator.box('trak',
        this.tkhd_(streamInfo), this.mdia_(streamInfo));
  }

  /**
   * Generate a TKHD box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  tkhd_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    const id = streamInfo.id + 1;
    let width = streamInfo.stream.width || 0;
    let height = streamInfo.stream.height || 0;
    if (streamInfo.type == ContentType.AUDIO) {
      width = 0;
      height = 0;
    }
    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 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
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  mdia_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    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_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    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 = streamInfo.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_(streamInfo.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
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  hdlr_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    let bytes = new Uint8Array([]);
    switch (streamInfo.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
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  minf_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    switch (streamInfo.type) {
      case ContentType.VIDEO:
        return Mp4Generator.box(
            'minf', Mp4Generator.box('vmhd', Mp4Generator.VMHD_),
            Mp4Generator.DINF_, this.stbl_(streamInfo));
      case ContentType.AUDIO:
        return Mp4Generator.box(
            'minf', Mp4Generator.box('smhd', Mp4Generator.SMHD_),
            Mp4Generator.DINF_, this.stbl_(streamInfo));
    }
    return new Uint8Array([]);
  }

  /**
   * Generate a STBL box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  stbl_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    return Mp4Generator.box(
        'stbl',
        this.stsd_(streamInfo),
        Mp4Generator.box('stts', Mp4Generator.STTS_),
        Mp4Generator.box('stsc', Mp4Generator.STSC_),
        Mp4Generator.box('stsz', Mp4Generator.STSZ_),
        Mp4Generator.box('stco', Mp4Generator.STCO_));
  }

  /**
   * Generate a STSD box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  stsd_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    let bytes = new Uint8Array([]);
    switch (streamInfo.type) {
      case ContentType.VIDEO:
        if (streamInfo.codecs.includes('avc1')) {
          bytes = this.avc1_(streamInfo);
        } else if (streamInfo.codecs.includes('hvc1')) {
          bytes = this.hvc1_(streamInfo);
        }
        break;
      case ContentType.AUDIO:
        if (streamInfo.codecs.includes('mp3')) {
          bytes = this.mp3_(streamInfo);
        } else if (streamInfo.codecs.includes('ac-3')) {
          bytes = this.ac3_(streamInfo);
        } else if (streamInfo.codecs.includes('ec-3')) {
          bytes = this.ec3_(streamInfo);
        } else {
          bytes = this.mp4a_(streamInfo);
        }
        break;
    }
    return Mp4Generator.box('stsd', Mp4Generator.STSD_, bytes);
  }

  /**
   * Generate a AVC1 box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  avc1_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;

    const width = streamInfo.stream.width || 0;
    const height = streamInfo.stream.height || 0;

    let avcCBox;
    if (streamInfo.videoConfig.byteLength > 0) {
      avcCBox = Mp4Generator.box('avcC', streamInfo.videoConfig);
    } else {
      avcCBox = Mp4Generator.box('avcC', this.avcC_(streamInfo));
    }

    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 boxName = 'avc1';
    let sinfBox = new Uint8Array([]);
    if (streamInfo.encrypted) {
      sinfBox = this.sinf_(streamInfo);
      boxName = 'encv';
    }
    return Mp4Generator.box(boxName, avc1Bytes, avcCBox, sinfBox);
  }

  /**
   * Generate a AVCC box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  avcC_(streamInfo) {
    const NALUTYPE_SPS = 7;
    const NALUTYPE_PPS = 8;

    // 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 < streamInfo.videoNalus.length; i++) {
      const naluBytes = this.hexStringToBuffer_(streamInfo.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;
    }
    return avcCBytes;
  }

  /**
   * Generate a HVC1 box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  hvc1_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;

    const width = streamInfo.stream.width || 0;
    const height = streamInfo.stream.height || 0;

    let hvcCBox = new Uint8Array([]);
    if (streamInfo.videoConfig.byteLength > 0) {
      hvcCBox = Mp4Generator.box('hvcC', streamInfo.videoConfig);
    }

    const hvc1Bytes = 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 boxName = 'hvc1';
    let sinfBox = new Uint8Array([]);
    if (streamInfo.encrypted) {
      sinfBox = this.sinf_(streamInfo);
      boxName = 'encv';
    }
    return Mp4Generator.box(boxName, hvc1Bytes, hvcCBox, sinfBox);
  }

  /**
   * Generate STSD bytes
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  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
      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 bytes;
  }

  /**
   * Generate a .MP3 box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  mp3_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    return Mp4Generator.box('.mp3', this.audioStsd_(streamInfo));
  }

  /**
   * Generate a AC-3 box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  ac3_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    const dac3Box = Mp4Generator.box('dac3', streamInfo.audioConfig);

    let boxName = 'ac-3';
    let sinfBox = new Uint8Array([]);
    if (streamInfo.encrypted) {
      sinfBox = this.sinf_(streamInfo);
      boxName = 'enca';
    }
    return Mp4Generator.box(boxName,
        this.audioStsd_(streamInfo), dac3Box, sinfBox);
  }

  /**
   * Generate a EC-3 box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  ec3_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    const dec3Box = Mp4Generator.box('dec3', streamInfo.audioConfig);

    let boxName = 'ec-3';
    let sinfBox = new Uint8Array([]);
    if (streamInfo.encrypted) {
      sinfBox = this.sinf_(streamInfo);
      boxName = 'enca';
    }
    return Mp4Generator.box(boxName,
        this.audioStsd_(streamInfo), dec3Box, sinfBox);
  }

  /**
   * Generate a MP4A box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  mp4a_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    let esdsBox;
    if (streamInfo.audioConfig.byteLength > 0) {
      esdsBox = Mp4Generator.box('esds', streamInfo.audioConfig);
    } else {
      esdsBox = Mp4Generator.box('esds', this.esds_(streamInfo));
    }

    let boxName = 'mp4a';
    let sinfBox = new Uint8Array([]);
    if (streamInfo.encrypted) {
      sinfBox = this.sinf_(streamInfo);
      boxName = 'enca';
    }
    return Mp4Generator.box(boxName,
        this.audioStsd_(streamInfo), esdsBox, sinfBox);
  }

  /**
   * Generate a ESDS box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  esds_(streamInfo) {
    const ContentType = shaka.util.ManifestParserUtils.ContentType;

    const id = streamInfo.id + 1;
    const channelsCount = streamInfo.stream.channelsCount || 2;
    const audioSamplingRate = streamInfo.stream.audioSamplingRate || 44100;

    const audioCodec = shaka.util.ManifestParserUtils.guessCodecs(
        ContentType.AUDIO, streamInfo.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, 0x00, 0x00, // bufferSizeDB
      0x00, 0x00, 0x00, 0x00, // maxBitrate
      0x00, 0x00, 0x00, 0x00, // 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;
    let trexs = new Uint8Array([]);
    for (const streamInfo of this.streamInfos_) {
      trexs = shaka.util.Uint8ArrayUtils.concat(trexs, this.trex_(streamInfo));
    }
    return Mp4Generator.box('mvex', trexs);
  }

  /**
   * Generate a TREX box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  trex_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    const id = streamInfo.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
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  pssh_(streamInfo) {
    let boxes = new Uint8Array([]);
    if (!streamInfo.encrypted) {
      return boxes;
    }

    for (const drmInfo of streamInfo.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
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  sinf_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    return Mp4Generator.box('sinf',
        this.frma_(streamInfo), this.schm_(), this.schi_(streamInfo));
  }

  /**
   * Generate a FRMA box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  frma_(streamInfo) {
    const codec = streamInfo.codecs.substring(
        0, streamInfo.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
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  schi_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    return Mp4Generator.box('schi', this.tenc_(streamInfo));
  }

  /**
   * Generate a TENC box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  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 streamInfo.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);
  }

  /**
   * Generate a Segment Data (MP4).
   *
   * @return {!Uint8Array}
   */
  segmentData() {
    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_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    return Mp4Generator.box('moof',
        this.mfhd_(streamInfo), this.traf_(streamInfo));
  }

  /**
   * Generate a MOOF box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  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_(sequenceNumber, 4),
    ]);
    return Mp4Generator.box('mfhd', bytes);
  }

  /**
   * Generate a TRAF box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  traf_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    const sampleDependencyTable = this.sdtp_(streamInfo);
    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_(streamInfo),
        this.tfdt_(streamInfo),
        this.trun_(streamInfo, offset),
        sampleDependencyTable);
  }

  /**
   * Generate a SDTP box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  sdtp_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    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 < samples.length; i++) {
      const flags = samples[i].flags;
      bytes[i + 4] = (flags.dependsOn << 4) |
          (flags.isDependedOn << 2) |
          flags.hasRedundancy;
    }
    return Mp4Generator.box('sdtp', bytes);
  }

  /**
   * Generate a TFHD box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  tfhd_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    const id = streamInfo.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
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @return {!Uint8Array}
   * @private
   */
  tfdt_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    const baseMediaDecodeTime =
        streamInfo.data ? streamInfo.data.baseMediaDecodeTime : 0;
    const upperWordBaseMediaDecodeTime =
      Math.floor(baseMediaDecodeTime / (Mp4Generator.UINT32_MAX_ + 1));
    const lowerWordBaseMediaDecodeTime =
      Math.floor(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('tfdt', bytes);
  }

  /**
   * Generate a TRUN box
   *
   * @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
   * @param {number} offset
   * @return {!Uint8Array}
   * @private
   */
  trun_(streamInfo, offset) {
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    const Mp4Generator = shaka.util.Mp4Generator;

    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 = streamInfo.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 = 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);
  }

  /**
   * Generate a MDAT box
   *
   * @return {!Uint8Array}
   * @private
   */
  mdat_(streamInfo) {
    const Mp4Generator = shaka.util.Mp4Generator;
    const samples = streamInfo.data ? streamInfo.data.samples : [];
    const allData = samples.map((sample) => sample.data);
    const bytes = shaka.util.Uint8ArrayUtils.concat(...allData);
    return Mp4Generator.box('mdat', bytes);
  }


  /**
   * @param {number} number
   * @param {number} numBytes
   * @return {!Array.<number>}
   * @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.<string, !Array.<number>>}
 */
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([]);

/**
 * @typedef {{
 *    id: number,
 *    type: string,
 *    codecs: string,
 *    encrypted: boolean,
 *    timescale: number,
 *    duration: number,
 *    videoNalus: !Array.<string>,
 *    audioConfig: !Uint8Array,
 *    videoConfig: !Uint8Array,
 *    data: ?shaka.util.Mp4Generator.Data,
 *    stream: !shaka.extern.Stream
 * }}
 *
 * @property {number} id
 *   A unique ID
 * @property {string} type
 *   Indicate the content type: 'video' or 'audio'.
 * @property {string} codecs
 *   <i>Defaults to '' (i.e., unknown / not needed).</i> <br>
 *   The Stream's codecs, e.g., 'avc1.4d4015' or 'vp9'<br>
 *   See {@link https://tools.ietf.org/html/rfc6381}
 * @property {boolean} encrypted
 *   Indicate if the stream is encrypted.
 * @property {number} timescale
 *   The Stream's timescale.
 * @property {number} duration
 *   The Stream's duration.
 * @property {!Array.<string>} videoNalus
 *   The stream's video nalus.
 * @property {!Uint8Array} audioConfig
 *   The stream's audio config.
 * @property {!Uint8Array} videoConfig
 *   The stream's video config.
 * @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.<shaka.util.Mp4Generator.Mp4Sample>
 * }}
 *
 * @property {number} sequenceNumber
 *   The sequence number.
 * @property {number} baseMediaDecodeTime
 *   The base media decode time.
 * @property {!Array.<shaka.util.Mp4Generator.Mp4Sample>} samples
 *   The data samples.
 */
shaka.util.Mp4Generator.Data;

/**
 * @typedef {{
 *    data: !Uint8Array,
 *    size: number,
 *    duration: number,
 *    cts: number,
 *    flags: !shaka.util.Mp4Generator.Mp4SampleFlags
 * }}
 *
 * @property {!Uint8Array} data
 *   The sample data.
 * @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;