Source: lib/transmuxer/mpeg_ts_transmuxer.js

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

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

goog.require('shaka.media.Capabilities');
goog.require('shaka.transmuxer.MpegAudio');
goog.require('shaka.transmuxer.TransmuxerEngine');
goog.require('shaka.util.BufferUtils');
goog.require('shaka.util.Error');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.MimeUtils');
goog.require('shaka.util.TsParser');
goog.require('shaka.util.Uint8ArrayUtils');

/**
 * @fileoverview
 *
 * This transmuxer takes an audio-only TS with MP3, and converts it to
 * raw MP3(audio/mpeg). We don't do it in ts_transmuxer.js because the
 * output of it is always MP4. This transmuxer is necessary because the only
 * browser that supports MP3 in MP4 is Firefox(audio/mp4; codecs="mp3"),
 * other browsers don't support it.
 */

/**
 * @implements {shaka.extern.Transmuxer}
 * @export
 */
shaka.transmuxer.MpegTsTransmuxer = class {
  /**
   * @param {string} mimeType
   */
  constructor(mimeType) {
    /** @private {string} */
    this.originalMimeType_ = mimeType;

    /** @private {?shaka.util.TsParser} */
    this.tsParser_ = null;
  }


  /**
   * @override
   * @export
   */
  destroy() {
    // Nothing
  }


  /**
   * Check if the mime type and the content type is supported.
   * @param {string} mimeType
   * @param {string=} contentType
   * @return {boolean}
   * @override
   * @export
   */
  isSupported(mimeType, contentType) {
    const Capabilities = shaka.media.Capabilities;

    if (!this.isTsContainer_(mimeType)) {
      return false;
    }

    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    const MimeUtils = shaka.util.MimeUtils;

    const codecs = MimeUtils.getCodecs(mimeType);
    const allCodecs = MimeUtils.splitCodecs(codecs);

    const audioCodec = shaka.util.ManifestParserUtils.guessCodecsSafe(
        ContentType.AUDIO, allCodecs);
    const videoCodec = shaka.util.ManifestParserUtils.guessCodecsSafe(
        ContentType.VIDEO, allCodecs);

    if (!audioCodec || videoCodec) {
      return false;
    }
    const normalizedCodec = MimeUtils.getNormalizedCodec(audioCodec);

    if (normalizedCodec != 'mp3') {
      return false;
    }

    return Capabilities.isTypeSupported(
        this.convertCodecs(ContentType.AUDIO, mimeType));
  }


  /**
   * Check if the mimetype is 'video/mp2t'.
   * @param {string} mimeType
   * @return {boolean}
   * @private
   */
  isTsContainer_(mimeType) {
    return mimeType.toLowerCase().split(';')[0] == 'video/mp2t';
  }


  /**
   * @override
   * @export
   */
  convertCodecs(contentType, mimeType) {
    if (this.isTsContainer_(mimeType)) {
      return 'audio/mpeg';
    }
    return mimeType;
  }


  /**
   * @override
   * @export
   */
  getOriginalMimeType() {
    return this.originalMimeType_;
  }


  /**
   * @override
   * @export
   */
  transmux(data, stream, reference, duration, contentType) {
    const MpegAudio = shaka.transmuxer.MpegAudio;
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;

    if (!this.tsParser_) {
      this.tsParser_ = new shaka.util.TsParser();
    } else {
      this.tsParser_.clearData();
    }

    const uint8ArrayData = shaka.util.BufferUtils.toUint8(data);

    const tsParser = this.tsParser_.parse(uint8ArrayData);
    const codecs = tsParser.getCodecs();
    if (codecs.audio != 'mp3' || contentType != ContentType.AUDIO) {
      return Promise.reject(new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.MEDIA,
          shaka.util.Error.Code.TRANSMUXING_FAILED));
    }

    let transmuxData = new Uint8Array([]);

    for (const audioData of tsParser.getAudioData()) {
      const data = audioData.data;
      if (!data) {
        continue;
      }
      let offset = 0;
      while (offset < data.length) {
        const header = MpegAudio.parseHeader(data, offset);
        if (!header) {
          offset++;
          continue;
        }
        if (offset + header.frameLength <= data.length) {
          transmuxData = Uint8ArrayUtils.concat(transmuxData,
              data.subarray(offset, offset + header.frameLength));
        }
        offset += header.frameLength;
      }
    }

    return Promise.resolve(transmuxData);
  }
};


shaka.transmuxer.TransmuxerEngine.registerTransmuxer(
    'video/mp2t',
    () => new shaka.transmuxer.MpegTsTransmuxer('video/mp2t'),
    shaka.transmuxer.TransmuxerEngine.PluginPriority.PREFERRED_SECONDARY);