Source: lib/transmuxer/matroska_transmuxer.js

/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

goog.provide('shaka.transmuxer.MatroskaTransmuxer');

goog.require('shaka.codec.AAC');
goog.require('shaka.codec.Ac3');
goog.require('shaka.codec.DolbyVision');
goog.require('shaka.codec.Ec3');
goog.require('shaka.codec.Flac');
goog.require('shaka.codec.MpegAudio');
goog.require('shaka.codec.Opus');
goog.require('shaka.codec.Vorbis');
goog.require('shaka.media.Capabilities');
goog.require('shaka.mkv.CodecId');
goog.require('shaka.mkv.MatroskaClusterParser');
goog.require('shaka.mkv.MatroskaCodecs');
goog.require('shaka.mkv.MatroskaConstants');
goog.require('shaka.mkv.MatroskaIndexParser');
goog.require('shaka.transmuxer.BaseTransmuxer');
goog.require('shaka.transmuxer.TransmuxerEngine');
goog.require('shaka.transmuxer.TransmuxerUtils');
goog.require('shaka.util.BufferUtils');
goog.require('shaka.util.Error');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.MimeUtils');
goog.require('shaka.util.Mp4Generator');
goog.require('shaka.util.Uint8ArrayUtils');
goog.require('shaka.util.WebmGenerator');
goog.requireType('shaka.media.SegmentReference');


/**
 * Repackages the frames of one Matroska track into what MSE plays: fragmented
 * MP4, or WebM for the codecs that browsers only take there (VP8, Vorbis).  The
 * encoded frames are copied as they are; nothing is decoded or transcoded.
 *
 * The initialization segment of a track (the beginning of the file, up to the
 * end of the track's TrackEntry) has no media to convert, so it only tells the
 * transmuxer what the track is.  The initialization segment that MSE gets is
 * produced together with the first media segment.
 *
 * @extends {shaka.transmuxer.BaseTransmuxer}
 * @implements {shaka.extern.Transmuxer}
 * @export
 */
shaka.transmuxer.MatroskaTransmuxer = class extends
  shaka.transmuxer.BaseTransmuxer {
  /**
   * @param {string} mimeType
   */
  constructor(mimeType) {
    super(mimeType);

    /**
     * The tracks and the time scale described by the last initialization
     * segment.
     * @private {?shaka.mkv.MatroskaIndexParser.Header}
     */
    this.header_ = null;

    /**
     * The track that is being converted.
     * @private {?shaka.mkv.MatroskaIndexParser.Track}
     */
    this.track_ = null;

    /**
     * The decode time at which the next segment is expected to start, in ticks
     * of the timescale, or null before the first segment.
     * @private {?number}
     */
    this.nextDecodeTime_ = null;

    /**
     * What the first audio segment showed about the audio stream.
     * @private {?shaka.transmuxer.MatroskaTransmuxer.AudioInfo}
     */
    this.audioInfo_ = null;
  }

  /**
   * @override
   * @export
   */
  destroy() {
    super.destroy();
    this.header_ = null;
    this.track_ = null;
    this.audioInfo_ = null;
  }

  /**
   * @param {string} mimeType
   * @param {string=} contentType
   * @return {boolean}
   * @override
   * @export
   */
  isSupported(mimeType, contentType) {
    if (this.getContainer_(mimeType) != this.getOriginalMimeType()) {
      return false;
    }
    const codecs = shaka.util.MimeUtils.getCodecs(mimeType);
    const MatroskaTransmuxer = shaka.transmuxer.MatroskaTransmuxer;
    if (!codecs || !MatroskaTransmuxer.isSupportedCodec_(codecs)) {
      return false;
    }
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    const type = contentType || (mimeType.startsWith('audio/') ?
        ContentType.AUDIO : ContentType.VIDEO);
    return shaka.media.Capabilities.isTypeSupported(
        this.convertCodecs(type, mimeType));
  }

  /**
   * @override
   * @export
   */
  convertCodecs(contentType, mimeType) {
    if (this.getContainer_(mimeType) != this.getOriginalMimeType()) {
      return mimeType;
    }
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    const MimeUtils = shaka.util.MimeUtils;
    const isAudio = contentType == ContentType.AUDIO;
    let codecs = MimeUtils.getCodecs(mimeType);
    if (isAudio && MimeUtils.getNormalizedCodec(codecs) == 'mp3' &&
        shaka.mkv.MatroskaCodecs.usesRawMpegAudio(shaka.mkv.CodecId.MP3)) {
      return 'audio/mpeg';
    }
    if (isAudio && MimeUtils.getNormalizedCodec(codecs) == 'mp3') {
      codecs = shaka.mkv.MatroskaCodecs.getMp3Mp4Codec() || codecs;
    }
    const isWebm = shaka.transmuxer.MatroskaTransmuxer.WEBM_CODECS_.some(
        (prefix) => codecs.toLowerCase().startsWith(prefix));
    const container = (isAudio ? 'audio' : 'video') +
        (isWebm ? '/webm' : '/mp4');
    if (isAudio) {
      codecs = MimeUtils.getCorrectAudioCodecs(codecs, container);
    }
    return MimeUtils.getFullType(container, codecs);
  }

  /**
   * @override
   * @export
   */
  transmux(data, stream, reference, duration, contentType) {
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    try {
      const bytes = shaka.util.BufferUtils.toUint8(data);
      if (!reference) {
        this.parseInit_(bytes, stream);
        // Nothing to append yet: see the description of the class.
        return Promise.resolve(new Uint8Array(0));
      }
      const trackNumber = shaka.mkv.MatroskaCodecs.getTrackNumber(stream.id);
      if (!this.header_ || !this.track_ || this.track_.number != trackNumber) {
        throw new Error('The initialization segment of track ' + trackNumber +
            ' has not been received');
      }
      const output = contentType == ContentType.AUDIO ?
          this.transmuxAudio_(bytes, stream, reference, duration) :
          this.transmuxVideo_(bytes, stream, reference, duration);
      return Promise.resolve(output);
    } catch (error) {
      if (error instanceof shaka.util.Error) {
        return Promise.reject(error);
      }
      return Promise.reject(new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.MEDIA,
          shaka.util.Error.Code.TRANSMUXING_FAILED,
          error && error.message ? error.message : String(error)));
    }
  }

  /**
   * Learns what the track is from its initialization segment.
   *
   * @param {!Uint8Array} bytes
   * @param {shaka.extern.Stream} stream
   * @private
   */
  parseInit_(bytes, stream) {
    const header = shaka.mkv.MatroskaIndexParser.parseHeader(bytes);
    const trackNumber = shaka.mkv.MatroskaCodecs.getTrackNumber(stream.id);
    const track = header.tracks.find((entry) => entry.number == trackNumber);
    if (!track) {
      throw new Error('Track ' + trackNumber + ' is not in the file');
    }
    this.header_ = header;
    this.track_ = track;
    // Everything derived from the previous track no longer applies.
    this.nextDecodeTime_ = null;
    this.audioInfo_ = null;
  }

  /**
   * @param {!Uint8Array} bytes
   * @param {shaka.extern.Stream} stream
   * @param {?shaka.media.SegmentReference} reference
   * @param {number} duration
   * @return {shaka.extern.TransmuxerOutput}
   * @private
   */
  transmuxVideo_(bytes, stream, reference, duration) {
    const MatroskaTransmuxer = shaka.transmuxer.MatroskaTransmuxer;
    const track = /** @type {!shaka.mkv.MatroskaIndexParser.Track} */(
      this.track_);
    const header = /** @type {!shaka.mkv.MatroskaIndexParser.Header} */(
      this.header_);
    const timescale = MatroskaTransmuxer.VIDEO_TIMESCALE_;

    if (shaka.mkv.MatroskaCodecs.usesWebm(track.codecId)) {
      return this.packageWebm_(bytes, stream, reference);
    }
    const frames = shaka.mkv.MatroskaClusterParser.parseFrames(
        bytes, track, header.timecodeScale);
    if (!frames.length) {
      return {data: new Uint8Array(0), init: null};
    }

    // Matroska stores only presentation times, and the frames come in decode
    // order.  With B-frames the two orders differ, and a frame must never be
    // decoded after the moment it is to be presented.  The frames decode in
    // the order of their presentation times, so those, sorted, are the decode
    // times.  The presentation times are moved later by the shift, which is
    // more than any encoder reorders frames.  As the decode times come from the
    // file alone, they continue from one segment to the next without any need
    // to know how the frames are reordered.
    const shift = Math.round(
        shaka.mkv.MatroskaConstants.MEDIA_TIME_SHIFT * timescale);
    const times = frames.map((frame) => Math.round(frame.time * timescale));
    const presentationTimes = times.map((time) => time + shift);
    const decodeTimes = times.slice().sort((a, b) => a - b);
    const offset = this.getContinuityOffset_(
        decodeTimes[0], header, timescale);

    const lastDuration = MatroskaTransmuxer.getLastDuration_(
        track, decodeTimes, timescale, reference);
    /** @type {!Array<shaka.util.Mp4Generator.Mp4Sample>} */
    const samples = [];
    for (let i = 0; i < frames.length; i++) {
      const decodeTime = decodeTimes[i] + offset;
      const nextDecodeTime = i + 1 < frames.length ?
          decodeTimes[i + 1] + offset : decodeTime + lastDuration;
      samples.push({
        data: frames[i].data,
        size: frames[i].data.byteLength,
        duration: nextDecodeTime - decodeTime,
        cts: presentationTimes[i] - decodeTime,
        flags: frames[i].keyframe ?
            shaka.transmuxer.TransmuxerUtils.VIDEO_KEYFRAME_FLAGS :
            shaka.transmuxer.TransmuxerUtils.VIDEO_NON_KEYFRAME_FLAGS,
      });
    }
    const baseMediaDecodeTime = decodeTimes[0] + offset;
    this.nextDecodeTime_ = baseMediaDecodeTime +
        samples.reduce((sum, sample) => sum + sample.duration, 0);

    // The stream is Dolby Vision if its codec string says so.
    const dolbyVisionEntry = track.dolbyVisionConfig ?
        shaka.codec.DolbyVision.getEntryName(stream.codecs) : null;
    const codecs = dolbyVisionEntry ||
        shaka.mkv.MatroskaCodecs.getMp4VideoCodec(track.codecId);
    const pixelAspectRatio =
        shaka.mkv.MatroskaCodecs.getPixelAspectRatio(track);
    /** @type {shaka.util.Mp4Generator.StreamInfo} */
    const streamInfo = {
      id: stream.id,
      type: shaka.util.ManifestParserUtils.ContentType.VIDEO,
      codecs,
      timescale,
      duration,
      mediaConfig: shaka.mkv.MatroskaCodecs.getVideoConfig(
          track, stream.codecs),
      hSpacing: pixelAspectRatio ? pixelAspectRatio.horizontal : undefined,
      vSpacing: pixelAspectRatio ? pixelAspectRatio.vertical : undefined,
      dolbyVisionConfig: dolbyVisionEntry ?
          /** @type {!Uint8Array} */(track.dolbyVisionConfig) : undefined,
      data: {
        sequenceNumber: this.frameIndex,
        baseMediaDecodeTime,
        samples,
      },
      stream,
    };
    return this.packageSegment(
        new shaka.util.Mp4Generator([streamInfo]), stream, reference);
  }

  /**
   * @param {!Uint8Array} bytes
   * @param {shaka.extern.Stream} stream
   * @param {?shaka.media.SegmentReference} reference
   * @param {number} duration
   * @return {shaka.extern.TransmuxerOutput}
   * @private
   */
  transmuxAudio_(bytes, stream, reference, duration) {
    const track = /** @type {!shaka.mkv.MatroskaIndexParser.Track} */(
      this.track_);
    const header = /** @type {!shaka.mkv.MatroskaIndexParser.Header} */(
      this.header_);

    if (shaka.mkv.MatroskaCodecs.usesWebm(track.codecId)) {
      return this.packageWebm_(bytes, stream, reference);
    }
    const frames = shaka.mkv.MatroskaClusterParser.parseFrames(
        bytes, track, header.timecodeScale);
    if (!frames.length) {
      return {data: new Uint8Array(0), init: null};
    }
    if (!this.audioInfo_) {
      this.audioInfo_ = shaka.transmuxer.MatroskaTransmuxer.getAudioInfo_(
          track, frames[0].data);
    }
    const info = this.audioInfo_;
    // What the decoder is going to see decides what the stream is, not what
    // the file announced.
    stream.audioSamplingRate = info.sampleRate;
    stream.channelsCount = info.channelCount;

    if (shaka.mkv.MatroskaCodecs.usesRawMpegAudio(track.codecId)) {
      return {
        data: shaka.util.Uint8ArrayUtils.concatRange(
            frames.map((frame) => frame.data)),
        init: null,
      };
    }

    /** @type {!Array<shaka.util.Mp4Generator.Mp4Sample>} */
    const samples = frames.map((frame) => {
      return {
        data: frame.data,
        size: frame.data.byteLength,
        duration: info.getDuration(frame.data),
        cts: 0,
        flags: shaka.transmuxer.TransmuxerUtils.AUDIO_SAMPLE_FLAGS,
      };
    });

    let baseMediaDecodeTime = Math.round(
        (frames[0].time + shaka.mkv.MatroskaConstants.MEDIA_TIME_SHIFT) *
        info.timescale);
    baseMediaDecodeTime += this.getContinuityOffset_(
        baseMediaDecodeTime, header, info.timescale);
    this.nextDecodeTime_ = baseMediaDecodeTime +
        samples.reduce((sum, sample) => sum + sample.duration, 0);

    /** @type {shaka.util.Mp4Generator.StreamInfo} */
    const streamInfo = {
      id: stream.id,
      type: shaka.util.ManifestParserUtils.ContentType.AUDIO,
      codecs: info.codecs,
      timescale: info.timescale,
      duration,
      mediaConfig: info.config,
      data: {
        sequenceNumber: this.frameIndex,
        baseMediaDecodeTime,
        samples,
      },
      stream,
    };
    return this.packageSegment(
        new shaka.util.Mp4Generator([streamInfo]), stream, reference);
  }

  /**
   * Writes the frames of a track as WebM.  WebM has presentation times only,
   * and the codecs it is used for here (VP8, Vorbis) do not reorder frames, so
   * they are what the file says, moved by the shift.
   *
   * @param {!Uint8Array} bytes
   * @param {shaka.extern.Stream} stream
   * @param {?shaka.media.SegmentReference} reference
   * @return {shaka.extern.TransmuxerOutput}
   * @private
   */
  packageWebm_(bytes, stream, reference) {
    const track = /** @type {!shaka.mkv.MatroskaIndexParser.Track} */(
      this.track_);
    const header = /** @type {!shaka.mkv.MatroskaIndexParser.Header} */(
      this.header_);
    const {NANOSECONDS_PER_SECOND, MEDIA_TIME_SHIFT} =
        shaka.mkv.MatroskaConstants;
    const ticksPerSecond = NANOSECONDS_PER_SECOND / header.timecodeScale;

    const frames = shaka.mkv.MatroskaClusterParser.parseFrames(
        bytes, track, header.timecodeScale);
    if (!frames.length) {
      return {data: new Uint8Array(0), init: null};
    }
    const times = this.getWebmTimes_(frames, track);

    /** @type {shaka.util.WebmGenerator.StreamInfo} */
    const streamInfo = {
      trackNumber: track.number,
      type: track.type,
      codecId: track.codecId,
      codecPrivate: track.codecPrivate,
      timecodeScale: header.timecodeScale,
      width: track.width || stream.width || 0,
      height: track.height || stream.height || 0,
      channelCount: track.channels || stream.channelsCount || 1,
      sampleRate: track.sampleRate || stream.audioSamplingRate || 0,
      frames: frames.map((frame, i) => {
        return {
          timecode: Math.round((times[i] + MEDIA_TIME_SHIFT) * ticksPerSecond),
          data: frame.data,
          keyframe: frame.keyframe,
        };
      }),
    };
    return this.packageSegment(
        new shaka.util.WebmGenerator(streamInfo), stream, reference);
  }

  /**
   * Gets the time of each frame.  Every block has one, and blocks written by
   * muxers often lace several frames of audio: MSE does not take lacing, so
   * each of those frames has to get a time of its own.  What is known of a
   * frame laced with others is the time of its block, which is the time of the
   * first, and (for the codecs that say it) how much audio each frame holds.
   *
   * @param {!Array<shaka.mkv.MatroskaClusterParser.Frame>} frames
   * @param {!shaka.mkv.MatroskaIndexParser.Track} track
   * @return {!Array<number>} The times in seconds.
   * @private
   */
  getWebmTimes_(frames, track) {
    const Vorbis = shaka.codec.Vorbis;
    const vorbisInfo = track.codecId == shaka.mkv.CodecId.VORBIS &&
        track.codecPrivate ? Vorbis.parseHeaders(track.codecPrivate) : null;
    const sampleRate = track.sampleRate || 0;

    const times = [];
    let previousWindow = 0;
    for (const frame of frames) {
      if (!vorbisInfo || !sampleRate || frame.laceIndex == 0) {
        // The time that the block writes, or that it spreads over its frames
        // if it says how long it lasts.
        times.push(frame.time);
        previousWindow = vorbisInfo ?
            Vorbis.getWindowSize(vorbisInfo, frame.data) : 0;
        continue;
      }
      // The next frame of a block of Vorbis starts when the audio of the one
      // before it is over.
      const window = Vorbis.getWindowSize(vorbisInfo, frame.data);
      times.push(times[times.length - 1] +
          Vorbis.getAdvance(previousWindow, window) / sampleRate);
      previousWindow = window;
    }
    return times;
  }

  /**
   * Segments that follow each other should have decode times that follow each
   * other too.  Times taken from Matroska have the resolution of its timecode
   * scale (one millisecond by default), so a segment's start can differ from
   * the end of the previous one by that much, which MSE would see as a tiny
   * gap or overlap.  This tells how much to move the start of a segment to
   * make it continue the previous one, when it is close enough to be the
   * next segment.
   *
   * @param {number} decodeTime The decode time the segment starts at.
   * @param {shaka.mkv.MatroskaIndexParser.Header} header
   * @param {number} timescale
   * @return {number} How much to add to the decode times, in ticks.
   * @private
   */
  getContinuityOffset_(decodeTime, header, timescale) {
    if (this.nextDecodeTime_ == null) {
      return 0;
    }
    const nanosecondsPerSecond =
        shaka.mkv.MatroskaConstants.NANOSECONDS_PER_SECOND;
    const ticks = shaka.transmuxer.MatroskaTransmuxer.CONTINUITY_TICKS_;
    // The rounding error of a time is up to half a tick of the timecode scale.
    // Allow two, for the ends of both segments.
    const tolerance = Math.ceil(
        ticks * header.timecodeScale / nanosecondsPerSecond * timescale);
    const offset = this.nextDecodeTime_ - decodeTime;
    return Math.abs(offset) <= tolerance ? offset : 0;
  }

  /**
   * Gets the duration of the last video sample of a segment, which has no
   * next frame to measure it against.
   *
   * @param {!shaka.mkv.MatroskaIndexParser.Track} track
   * @param {!Array<number>} decodeTimes
   * @param {number} timescale
   * @param {?shaka.media.SegmentReference} reference
   * @return {number}
   * @private
   */
  static getLastDuration_(track, decodeTimes, timescale, reference) {
    if (track.defaultDuration) {
      return Math.round(track.defaultDuration * timescale);
    }
    const count = decodeTimes.length;
    if (count > 1) {
      return decodeTimes[count - 1] - decodeTimes[count - 2];
    }
    // A lone frame: it can only last as long as its segment.
    return reference ?
        Math.round((reference.endTime - reference.startTime) * timescale) : 0;
  }

  /**
   * Works out how to describe an audio track in MP4.
   *
   * @param {!shaka.mkv.MatroskaIndexParser.Track} track
   * @param {!Uint8Array} firstFrame The first frame of the track.
   * @return {!shaka.transmuxer.MatroskaTransmuxer.AudioInfo}
   * @private
   */
  static getAudioInfo_(track, firstFrame) {
    const CodecId = shaka.mkv.CodecId;
    const MatroskaCodecs = shaka.mkv.MatroskaCodecs;
    const MatroskaTransmuxer = shaka.transmuxer.MatroskaTransmuxer;
    const sampleRate = track.sampleRate || 0;
    const channelCount = track.channels || 0;

    switch (track.codecId) {
      case CodecId.AAC: {
        if (!sampleRate || !track.codecPrivate) {
          throw new Error('The AAC track has no sample rate or configuration');
        }
        return {
          codecs: MatroskaCodecs.getCodecs(track) || '',
          timescale: sampleRate,
          sampleRate,
          channelCount,
          config: shaka.codec.AAC.buildEsds(track.codecPrivate),
          getDuration: () => MatroskaTransmuxer.AAC_SAMPLES_PER_FRAME_,
        };
      }
      case CodecId.AC3:
      case CodecId.EAC3: {
        const isEc3 = track.codecId == CodecId.EAC3;
        // The configuration boxes (dac3, dec3) describe the first frame.
        /** @type {!shaka.codec.Ac3.Ac3Frame} */
        const frame = {
          sampleRate: 0,
          channelCount: 0,
          audioConfig: new Uint8Array(0),
          frameLength: 0,
        };
        const parsed = isEc3 ?
            shaka.codec.Ec3.parseFrame(firstFrame, 0, frame) :
            shaka.codec.Ac3.parseFrame(firstFrame, 0, frame);
        if (!parsed || !frame.sampleRate) {
          throw new Error('The first Dolby Digital frame is invalid');
        }
        const samplesPerFrame = isEc3 ?
            shaka.codec.Ec3.EC3_SAMPLES_PER_FRAME :
            shaka.codec.Ac3.AC3_SAMPLES_PER_FRAME;
        return {
          codecs: isEc3 ? 'ec-3' : 'ac-3',
          timescale: frame.sampleRate,
          sampleRate: frame.sampleRate,
          channelCount: frame.channelCount,
          config: frame.audioConfig,
          getDuration: () => samplesPerFrame,
        };
      }
      case CodecId.OPUS:
        return {
          codecs: 'opus',
          timescale: shaka.codec.Opus.SAMPLE_RATE,
          sampleRate: shaka.codec.Opus.SAMPLE_RATE,
          channelCount,
          config: MatroskaCodecs.getOpusConfig(track),
          // A packet holds a variable amount of audio, given by its TOC.
          getDuration: (frame) => shaka.codec.Opus.getPacketSampleCount(frame),
        };
      case CodecId.FLAC: {
        const parsedInfo = track.codecPrivate &&
            shaka.codec.Flac.parseStreamInfo(track.codecPrivate);
        if (!parsedInfo || !track.codecPrivate) {
          throw new Error('The FLAC track has no STREAMINFO');
        }
        const flacInfo = /** @type {shaka.codec.Flac.StreamInfo} */(
          parsedInfo);
        return {
          codecs: 'flac',
          timescale: flacInfo.sampleRate,
          sampleRate: flacInfo.sampleRate,
          channelCount: flacInfo.channelCount,
          config: shaka.codec.Flac.buildDfLa(track.codecPrivate),
          // Each frame says how many samples it has.
          getDuration: (frame) => {
            return shaka.codec.Flac.getBlockSize(frame, flacInfo);
          },
        };
      }
      case CodecId.MP3: {
        const parsed = shaka.codec.MpegAudio.parseHeader(firstFrame, 0);
        if (!parsed) {
          throw new Error('The first MP3 frame is invalid');
        }
        // MP4 takes MP3 as "mp3" (a sample entry without configuration) or,
        // on the platforms that need it, as MPEG audio with its esds.
        const mp4Codec = shaka.mkv.MatroskaCodecs.getMp3Mp4Codec();
        const usesEsds = mp4Codec != null && mp4Codec != 'mp3';
        return {
          codecs: usesEsds ?
              shaka.codec.MpegAudio.getMp4Codec(parsed.sampleRate) : 'mp3',
          timescale: parsed.sampleRate,
          sampleRate: parsed.sampleRate,
          channelCount: parsed.channelCount,
          config: usesEsds ? shaka.codec.MpegAudio.buildEsds(
              parsed.sampleRate) : new Uint8Array(0),
          getDuration: () => parsed.samplesPerFrame,
        };
      }
    }
    throw new Error('Unsupported audio codec ' + track.codecId);
  }

  /**
   * @param {string} mimeType
   * @return {string} The MIME type without the parameters, in lower case.
   * @private
   */
  getContainer_(mimeType) {
    return mimeType.toLowerCase().split(';')[0];
  }

  /**
   * @param {string} codecs
   * @return {boolean}
   * @private
   */
  static isSupportedCodec_(codecs) {
    const prefixes = shaka.transmuxer.MatroskaTransmuxer.SUPPORTED_CODECS_;
    return prefixes.some((prefix) => codecs.toLowerCase().startsWith(prefix));
  }
};


/**
 * @typedef {{
 *   codecs: string,
 *   timescale: number,
 *   sampleRate: number,
 *   channelCount: number,
 *   config: !Uint8Array,
 *   getDuration: function(!Uint8Array):number,
 * }}
 *
 * @property {string} codecs
 *   The codec, as Mp4Generator names it.
 * @property {number} timescale
 *   The ticks per second the media times are written in.
 * @property {number} sampleRate
 * @property {number} channelCount
 * @property {!Uint8Array} config
 *   The payload of the codec configuration box.
 * @property {function(!Uint8Array):number} getDuration
 *   Returns the duration, in ticks, of a frame.
 */
shaka.transmuxer.MatroskaTransmuxer.AudioInfo;


/**
 * The clock of MPEG video, 90 kHz.  Video frame durations (1/24, 1/25, 1/30,
 * 1/60 s and their 1001/1000 variants) are whole numbers of ticks at this rate,
 * and it leaves plenty of room in the 64-bit decode times.
 *
 * @private @const {number}
 */
shaka.transmuxer.MatroskaTransmuxer.VIDEO_TIMESCALE_ = 90000;


/**
 * The number of samples an AAC frame codes (AAC-LC and HE-AAC alike, counted
 * at the rate of the AAC core).
 *
 * @private @const {number}
 */
shaka.transmuxer.MatroskaTransmuxer.AAC_SAMPLES_PER_FRAME_ = 1024;


/**
 * How many ticks of the timecode scale apart two adjoining segments may be for
 * the transmuxer to consider them continuous: half a tick of rounding error at
 * the end of one and at the start of the other.
 *
 * @private @const {number}
 */
shaka.transmuxer.MatroskaTransmuxer.CONTINUITY_TICKS_ = 2;


/**
 * The prefixes of the codec strings that can be repackaged.  They are the
 * codecs MP4 can carry and that MSE plays; see shaka.mkv.MatroskaCodecs.
 *
 * @private @const {!Array<string>}
 */
shaka.transmuxer.MatroskaTransmuxer.SUPPORTED_CODECS_ = [
  'avc1.',
  'hvc1.',
  'av01.',
  'vp09.',
  'dvh1.',
  'dva1.',
  'dav1.',
  'mp4a.40.',
  'ac-3',
  'ec-3',
  'opus',
  'mp3',
  'flac',
  'vp8',
  'vorbis',
];


/**
 * The prefixes of the codec strings that are written as WebM; the others are
 * written as MP4.
 *
 * @private @const {!Array<string>}
 */
shaka.transmuxer.MatroskaTransmuxer.WEBM_CODECS_ = ['vp8', 'vorbis'];


shaka.transmuxer.TransmuxerEngine.registerTransmuxer(
    'video/x-matroska',
    () => new shaka.transmuxer.MatroskaTransmuxer('video/x-matroska'),
    shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);
shaka.transmuxer.TransmuxerEngine.registerTransmuxer(
    'audio/x-matroska',
    () => new shaka.transmuxer.MatroskaTransmuxer('audio/x-matroska'),
    shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);