OutputConsumerAdapterV30.java

/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.media3.exoplayer.source.mediaparser;

import static android.media.MediaParser.PARSER_NAME_AC3;
import static android.media.MediaParser.PARSER_NAME_AC4;
import static android.media.MediaParser.PARSER_NAME_ADTS;
import static android.media.MediaParser.PARSER_NAME_AMR;
import static android.media.MediaParser.PARSER_NAME_FLAC;
import static android.media.MediaParser.PARSER_NAME_FLV;
import static android.media.MediaParser.PARSER_NAME_FMP4;
import static android.media.MediaParser.PARSER_NAME_MATROSKA;
import static android.media.MediaParser.PARSER_NAME_MP3;
import static android.media.MediaParser.PARSER_NAME_MP4;
import static android.media.MediaParser.PARSER_NAME_OGG;
import static android.media.MediaParser.PARSER_NAME_PS;
import static android.media.MediaParser.PARSER_NAME_TS;
import static android.media.MediaParser.PARSER_NAME_WAV;

import android.annotation.SuppressLint;
import android.media.DrmInitData.SchemeInitData;
import android.media.MediaCodec;
import android.media.MediaCodec.CryptoInfo;
import android.media.MediaFormat;
import android.media.MediaParser;
import android.media.MediaParser.TrackData;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.C;
import androidx.media3.common.C.SelectionFlags;
import androidx.media3.common.ColorInfo;
import androidx.media3.common.DataReader;
import androidx.media3.common.DrmInitData;
import androidx.media3.common.DrmInitData.SchemeData;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.TimestampAdjuster;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.extractor.ChunkIndex;
import androidx.media3.extractor.DummyExtractorOutput;
import androidx.media3.extractor.ExtractorOutput;
import androidx.media3.extractor.SeekMap;
import androidx.media3.extractor.SeekPoint;
import androidx.media3.extractor.TrackOutput;
import androidx.media3.extractor.TrackOutput.CryptoData;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.LongBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.compatqual.NullableType;

/**
 * {@link MediaParser.OutputConsumer} implementation that redirects output to an {@link
 * ExtractorOutput}.
 */
@RequiresApi(30)
@SuppressLint("Override") // TODO: Remove once the SDK becomes stable.
@UnstableApi
public final class OutputConsumerAdapterV30 implements MediaParser.OutputConsumer {

  private static final String TAG = "OConsumerAdapterV30";

  private static final Pair<MediaParser.SeekPoint, MediaParser.SeekPoint> SEEK_POINT_PAIR_START =
      Pair.create(MediaParser.SeekPoint.START, MediaParser.SeekPoint.START);
  private static final String MEDIA_FORMAT_KEY_TRACK_TYPE = "track-type-string";
  private static final String MEDIA_FORMAT_KEY_CHUNK_INDEX_SIZES = "chunk-index-int-sizes";
  private static final String MEDIA_FORMAT_KEY_CHUNK_INDEX_OFFSETS = "chunk-index-long-offsets";
  private static final String MEDIA_FORMAT_KEY_CHUNK_INDEX_DURATIONS =
      "chunk-index-long-us-durations";
  private static final String MEDIA_FORMAT_KEY_CHUNK_INDEX_TIMES = "chunk-index-long-us-times";
  private static final Pattern REGEX_CRYPTO_INFO_PATTERN =
      Pattern.compile("pattern \(encrypt: (\d+), skip: (\d+)\)");

  private final ArrayList<@NullableType TrackOutput> trackOutputs;
  private final ArrayList<@NullableType Format> trackFormats;
  private final ArrayList<@NullableType CryptoInfo> lastReceivedCryptoInfos;
  private final ArrayList<@NullableType CryptoData> lastOutputCryptoDatas;
  private final DataReaderAdapter scratchDataReaderAdapter;
  private final boolean expectDummySeekMap;
  private final @C.TrackType int primaryTrackType;
  @Nullable private final Format primaryTrackManifestFormat;

  private ExtractorOutput extractorOutput;
  @Nullable private MediaParser.SeekMap dummySeekMap;
  @Nullable private MediaParser.SeekMap lastSeekMap;
  @Nullable private String containerMimeType;
  @Nullable private ChunkIndex lastChunkIndex;
  @Nullable private TimestampAdjuster timestampAdjuster;
  private List<Format> muxedCaptionFormats;
  private int primaryTrackIndex;
  private long sampleTimestampUpperLimitFilterUs;
  private boolean tracksFoundCalled;
  private boolean tracksEnded;
  private boolean seekingDisabled;

  /**
   * Equivalent to {@link #OutputConsumerAdapterV30(Format, int, boolean)
   * OutputConsumerAdapterV30(primaryTrackManifestFormat= null, primaryTrackType= C.TRACK_TYPE_NONE,
   * expectDummySeekMap= false)}
   */
  public OutputConsumerAdapterV30() {
    this(
        /* primaryTrackManifestFormat= */ null,
        /* primaryTrackType= */ C.TRACK_TYPE_NONE,
        /* expectDummySeekMap= */ false);
  }

  /**
   * Creates a new instance.
   *
   * @param primaryTrackManifestFormat The manifest-obtained format of the primary track, or null if
   *     not applicable.
   * @param primaryTrackType The {@link C.TrackType type} of the primary track. {@link
   *     C#TRACK_TYPE_NONE} if there is no primary track.
   * @param expectDummySeekMap Whether the output consumer should expect an initial dummy seek map
   *     which should be exposed through {@link #getDummySeekMap()}.
   */
  public OutputConsumerAdapterV30(
      @Nullable Format primaryTrackManifestFormat,
      @C.TrackType int primaryTrackType,
      boolean expectDummySeekMap) {
    this.expectDummySeekMap = expectDummySeekMap;
    this.primaryTrackManifestFormat = primaryTrackManifestFormat;
    this.primaryTrackType = primaryTrackType;
    trackOutputs = new ArrayList<>();
    trackFormats = new ArrayList<>();
    lastReceivedCryptoInfos = new ArrayList<>();
    lastOutputCryptoDatas = new ArrayList<>();
    scratchDataReaderAdapter = new DataReaderAdapter();
    extractorOutput = new DummyExtractorOutput();
    sampleTimestampUpperLimitFilterUs = C.TIME_UNSET;
    muxedCaptionFormats = ImmutableList.of();
  }

  /**
   * Sets an upper limit for sample timestamp filtering.
   *
   * <p>When set, samples with timestamps greater than {@code sampleTimestampUpperLimitFilterUs}
   * will be discarded.
   *
   * @param sampleTimestampUpperLimitFilterUs The maximum allowed sample timestamp, or {@link
   *     C#TIME_UNSET} to remove filtering.
   */
  public void setSampleTimestampUpperLimitFilterUs(long sampleTimestampUpperLimitFilterUs) {
    this.sampleTimestampUpperLimitFilterUs = sampleTimestampUpperLimitFilterUs;
  }

  /** Sets a {@link TimestampAdjuster} for adjusting the timestamps of the output samples. */
  public void setTimestampAdjuster(TimestampAdjuster timestampAdjuster) {
    this.timestampAdjuster = timestampAdjuster;
  }

  /**
   * Sets the {@link ExtractorOutput} to which {@link MediaParser MediaParser's} output is directed.
   */
  public void setExtractorOutput(ExtractorOutput extractorOutput) {
    this.extractorOutput = extractorOutput;
  }

  /** Sets {@link Format} information associated to the caption tracks multiplexed in the media. */
  public void setMuxedCaptionFormats(List<Format> muxedCaptionFormats) {
    this.muxedCaptionFormats = muxedCaptionFormats;
  }

  /** Overrides future received {@link SeekMap SeekMaps} with non-seekable instances. */
  public void disableSeeking() {
    seekingDisabled = true;
  }

  /**
   * Returns a dummy {@link MediaParser.SeekMap}, or null if not available.
   *
   * <p>the dummy {@link MediaParser.SeekMap} returns a single {@link MediaParser.SeekPoint} whose
   * {@link MediaParser.SeekPoint#timeMicros} matches the requested timestamp, and {@link
   * MediaParser.SeekPoint#position} is 0.
   */
  @Nullable
  public MediaParser.SeekMap getDummySeekMap() {
    return dummySeekMap;
  }

  /** Returns the most recently output {@link ChunkIndex}, or null if none has been output. */
  @Nullable
  public ChunkIndex getChunkIndex() {
    return lastChunkIndex;
  }

  /**
   * Returns the {@link MediaParser.SeekPoint} instances corresponding to the given timestamp.
   *
   * @param seekTimeUs The timestamp in microseconds to retrieve {@link MediaParser.SeekPoint}
   *     instances for.
   * @return The {@link MediaParser.SeekPoint} instances corresponding to the given timestamp.
   */
  public Pair<MediaParser.SeekPoint, MediaParser.SeekPoint> getSeekPoints(long seekTimeUs) {
    return lastSeekMap != null ? lastSeekMap.getSeekPoints(seekTimeUs) : SEEK_POINT_PAIR_START;
  }

  /**
   * Defines the container mime type to propagate through {@link TrackOutput#format}.
   *
   * @param parserName The name of the selected parser.
   */
  public void setSelectedParserName(String parserName) {
    containerMimeType = getMimeType(parserName);
  }

  /**
   * Returns the last output format for each track, or null if not all the tracks have been
   * identified.
   */
  @Nullable
  public Format[] getSampleFormats() {
    if (!tracksFoundCalled) {
      return null;
    }
    Format[] sampleFormats = new Format[trackFormats.size()];
    for (int i = 0; i < trackFormats.size(); i++) {
      sampleFormats[i] = Assertions.checkNotNull(trackFormats.get(i));
    }
    return sampleFormats;
  }

  // MediaParser.OutputConsumer implementation.

  @Override
  public void onTrackCountFound(int numberOfTracks) {
    tracksFoundCalled = true;
    maybeEndTracks();
  }

  @Override
  public void onSeekMapFound(MediaParser.SeekMap seekMap) {
    if (expectDummySeekMap && dummySeekMap == null) {
      // This is a dummy seek map.
      dummySeekMap = seekMap;
    } else {
      lastSeekMap = seekMap;
      long durationUs = seekMap.getDurationMicros();
      extractorOutput.seekMap(
          seekingDisabled
              ? new SeekMap.Unseekable(
                  durationUs != MediaParser.SeekMap.UNKNOWN_DURATION ? durationUs : C.TIME_UNSET)
              : new SeekMapAdapter(seekMap));
    }
  }

  @Override
  public void onTrackDataFound(int trackIndex, TrackData trackData) {
    if (maybeObtainChunkIndex(trackData.mediaFormat)) {
      // The MediaFormat contains a chunk index. It does not contain anything else.
      return;
    }

    ensureSpaceForTrackIndex(trackIndex);
    @Nullable TrackOutput trackOutput = trackOutputs.get(trackIndex);
    if (trackOutput == null) {
      @Nullable
      String trackTypeString = trackData.mediaFormat.getString(MEDIA_FORMAT_KEY_TRACK_TYPE);
      int trackType =
          toTrackTypeConstant(
              trackTypeString != null
                  ? trackTypeString
                  : trackData.mediaFormat.getString(MediaFormat.KEY_MIME));
      if (trackType == primaryTrackType) {
        primaryTrackIndex = trackIndex;
      }
      trackOutput = extractorOutput.track(trackIndex, trackType);
      trackOutputs.set(trackIndex, trackOutput);
      if (trackTypeString != null) {
        // The MediaFormat includes the track type string, so it cannot include any other keys, as
        // per the android.media.mediaparser.eagerlyExposeTrackType parameter documentation.
        return;
      }
    }
    Format format = toExoPlayerFormat(trackData);
    trackOutput.format(
        primaryTrackManifestFormat != null && trackIndex == primaryTrackIndex
            ? format.withManifestFormatInfo(primaryTrackManifestFormat)
            : format);
    trackFormats.set(trackIndex, format);
    maybeEndTracks();
  }

  @Override
  public void onSampleDataFound(int trackIndex, MediaParser.InputReader sampleData)
      throws IOException {
    ensureSpaceForTrackIndex(trackIndex);
    scratchDataReaderAdapter.input = sampleData;
    TrackOutput trackOutput = trackOutputs.get(trackIndex);
    if (trackOutput == null) {
      trackOutput = extractorOutput.track(trackIndex, C.TRACK_TYPE_UNKNOWN);
      trackOutputs.set(trackIndex, trackOutput);
    }
    trackOutput.sampleData(
        scratchDataReaderAdapter, (int) sampleData.getLength(), /* allowEndOfInput= */ true);
  }

  @Override
  public void onSampleCompleted(
      int trackIndex,
      long timeUs,
      int flags,
      int size,
      int offset,
      @Nullable MediaCodec.CryptoInfo cryptoInfo) {
    if (sampleTimestampUpperLimitFilterUs != C.TIME_UNSET
        && timeUs >= sampleTimestampUpperLimitFilterUs) {
      // Ignore this sample.
      return;
    } else if (timestampAdjuster != null) {
      timeUs = timestampAdjuster.adjustSampleTimestamp(timeUs);
    }
    Assertions.checkNotNull(trackOutputs.get(trackIndex))
        .sampleMetadata(timeUs, flags, size, offset, toExoPlayerCryptoData(trackIndex, cryptoInfo));
  }

  // Private methods.

  private boolean maybeObtainChunkIndex(MediaFormat mediaFormat) {
    @Nullable
    ByteBuffer chunkIndexSizesByteBuffer =
        mediaFormat.getByteBuffer(MEDIA_FORMAT_KEY_CHUNK_INDEX_SIZES);
    if (chunkIndexSizesByteBuffer == null) {
      return false;
    }
    IntBuffer chunkIndexSizes = chunkIndexSizesByteBuffer.asIntBuffer();
    LongBuffer chunkIndexOffsets =
        Assertions.checkNotNull(mediaFormat.getByteBuffer(MEDIA_FORMAT_KEY_CHUNK_INDEX_OFFSETS))
            .asLongBuffer();
    LongBuffer chunkIndexDurationsUs =
        Assertions.checkNotNull(mediaFormat.getByteBuffer(MEDIA_FORMAT_KEY_CHUNK_INDEX_DURATIONS))
            .asLongBuffer();
    LongBuffer chunkIndexTimesUs =
        Assertions.checkNotNull(mediaFormat.getByteBuffer(MEDIA_FORMAT_KEY_CHUNK_INDEX_TIMES))
            .asLongBuffer();
    int[] sizes = new int[chunkIndexSizes.remaining()];
    long[] offsets = new long[chunkIndexOffsets.remaining()];
    long[] durationsUs = new long[chunkIndexDurationsUs.remaining()];
    long[] timesUs = new long[chunkIndexTimesUs.remaining()];
    chunkIndexSizes.get(sizes);
    chunkIndexOffsets.get(offsets);
    chunkIndexDurationsUs.get(durationsUs);
    chunkIndexTimesUs.get(timesUs);
    lastChunkIndex = new ChunkIndex(sizes, offsets, durationsUs, timesUs);
    extractorOutput.seekMap(lastChunkIndex);
    return true;
  }

  private void ensureSpaceForTrackIndex(int trackIndex) {
    for (int i = trackOutputs.size(); i <= trackIndex; i++) {
      trackOutputs.add(null);
      trackFormats.add(null);
      lastReceivedCryptoInfos.add(null);
      lastOutputCryptoDatas.add(null);
    }
  }

  @Nullable
  private CryptoData toExoPlayerCryptoData(int trackIndex, @Nullable CryptoInfo cryptoInfo) {
    if (cryptoInfo == null) {
      return null;
    }

    @Nullable CryptoInfo lastReceivedCryptoInfo = lastReceivedCryptoInfos.get(trackIndex);
    CryptoData cryptoDataToOutput;
    // MediaParser keeps identity and value equality aligned for efficient comparison.
    if (lastReceivedCryptoInfo == cryptoInfo) {
      // They match, we can reuse the last one we created.
      cryptoDataToOutput = Assertions.checkNotNull(lastOutputCryptoDatas.get(trackIndex));
    } else {
      // They don't match, we create a new CryptoData.

      // TODO: Access pattern encryption info directly once the Android SDK makes it visible.
      // See [Internal ref: b/154248283].
      int encryptedBlocks;
      int clearBlocks;
      try {
        Matcher matcher = REGEX_CRYPTO_INFO_PATTERN.matcher(cryptoInfo.toString());
        matcher.find();
        encryptedBlocks = Integer.parseInt(Util.castNonNull(matcher.group(1)));
        clearBlocks = Integer.parseInt(Util.castNonNull(matcher.group(2)));
      } catch (RuntimeException e) {
        // Should never happen.
        Log.e(TAG, "Unexpected error while parsing CryptoInfo: " + cryptoInfo, e);
        // Assume no-pattern encryption.
        encryptedBlocks = 0;
        clearBlocks = 0;
      }
      cryptoDataToOutput =
          new CryptoData(cryptoInfo.mode, cryptoInfo.key, encryptedBlocks, clearBlocks);
      lastReceivedCryptoInfos.set(trackIndex, cryptoInfo);
      lastOutputCryptoDatas.set(trackIndex, cryptoDataToOutput);
    }
    return cryptoDataToOutput;
  }

  private void maybeEndTracks() {
    if (!tracksFoundCalled || tracksEnded) {
      return;
    }
    int size = trackOutputs.size();
    for (int i = 0; i < size; i++) {
      if (trackOutputs.get(i) == null) {
        return;
      }
    }
    extractorOutput.endTracks();
    tracksEnded = true;
  }

  private static @C.TrackType int toTrackTypeConstant(@Nullable String string) {
    if (string == null) {
      return C.TRACK_TYPE_UNKNOWN;
    }
    switch (string) {
      case "audio":
        return C.TRACK_TYPE_AUDIO;
      case "video":
        return C.TRACK_TYPE_VIDEO;
      case "text":
        return C.TRACK_TYPE_TEXT;
      case "metadata":
        return C.TRACK_TYPE_METADATA;
      case "unknown":
        return C.TRACK_TYPE_UNKNOWN;
      default:
        // Must be a MIME type.
        return MimeTypes.getTrackType(string);
    }
  }

  private Format toExoPlayerFormat(TrackData trackData) {
    // TODO: Consider adding support for the following:
    //    format.id
    //    format.stereoMode
    //    format.projectionData
    MediaFormat mediaFormat = trackData.mediaFormat;
    @Nullable String mediaFormatMimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
    int mediaFormatAccessibilityChannel =
        mediaFormat.getInteger(
            MediaFormat.KEY_CAPTION_SERVICE_NUMBER, /* defaultValue= */ Format.NO_VALUE);
    Format.Builder formatBuilder =
        new Format.Builder()
            .setDrmInitData(
                toExoPlayerDrmInitData(
                    mediaFormat.getString("crypto-mode-fourcc"), trackData.drmInitData))
            .setContainerMimeType(containerMimeType)
            .setPeakBitrate(
                mediaFormat.getInteger(
                    MediaFormat.KEY_BIT_RATE, /* defaultValue= */ Format.NO_VALUE))
            .setChannelCount(
                mediaFormat.getInteger(
                    MediaFormat.KEY_CHANNEL_COUNT, /* defaultValue= */ Format.NO_VALUE))
            .setColorInfo(getColorInfo(mediaFormat))
            .setSampleMimeType(mediaFormatMimeType)
            .setCodecs(mediaFormat.getString(MediaFormat.KEY_CODECS_STRING))
            .setFrameRate(
                mediaFormat.getFloat(
                    MediaFormat.KEY_FRAME_RATE, /* defaultValue= */ Format.NO_VALUE))
            .setWidth(
                mediaFormat.getInteger(MediaFormat.KEY_WIDTH, /* defaultValue= */ Format.NO_VALUE))
            .setHeight(
                mediaFormat.getInteger(MediaFormat.KEY_HEIGHT, /* defaultValue= */ Format.NO_VALUE))
            .setInitializationData(getInitializationData(mediaFormat))
            .setLanguage(mediaFormat.getString(MediaFormat.KEY_LANGUAGE))
            .setMaxInputSize(
                mediaFormat.getInteger(
                    MediaFormat.KEY_MAX_INPUT_SIZE, /* defaultValue= */ Format.NO_VALUE))
            .setPcmEncoding(
                mediaFormat.getInteger("exo-pcm-encoding", /* defaultValue= */ Format.NO_VALUE))
            .setRotationDegrees(
                mediaFormat.getInteger(MediaFormat.KEY_ROTATION, /* defaultValue= */ 0))
            .setSampleRate(
                mediaFormat.getInteger(
                    MediaFormat.KEY_SAMPLE_RATE, /* defaultValue= */ Format.NO_VALUE))
            .setSelectionFlags(getSelectionFlags(mediaFormat))
            .setEncoderDelay(
                mediaFormat.getInteger(MediaFormat.KEY_ENCODER_DELAY, /* defaultValue= */ 0))
            .setEncoderPadding(
                mediaFormat.getInteger(MediaFormat.KEY_ENCODER_PADDING, /* defaultValue= */ 0))
            .setPixelWidthHeightRatio(
                mediaFormat.getFloat("pixel-width-height-ratio-float", /* defaultValue= */ 1f))
            .setSubsampleOffsetUs(
                mediaFormat.getLong(
                    "subsample-offset-us-long", /* defaultValue= */ Format.OFFSET_SAMPLE_RELATIVE))
            .setAccessibilityChannel(mediaFormatAccessibilityChannel);
    for (int i = 0; i < muxedCaptionFormats.size(); i++) {
      Format muxedCaptionFormat = muxedCaptionFormats.get(i);
      if (Util.areEqual(muxedCaptionFormat.sampleMimeType, mediaFormatMimeType)
          && muxedCaptionFormat.accessibilityChannel == mediaFormatAccessibilityChannel) {
        // The track's format matches this muxedCaptionFormat, so we apply the manifest format
        // information to the track.
        formatBuilder
            .setLanguage(muxedCaptionFormat.language)
            .setRoleFlags(muxedCaptionFormat.roleFlags)
            .setSelectionFlags(muxedCaptionFormat.selectionFlags)
            .setLabel(muxedCaptionFormat.label)
            .setMetadata(muxedCaptionFormat.metadata);
        break;
      }
    }
    return formatBuilder.build();
  }

  @Nullable
  private static DrmInitData toExoPlayerDrmInitData(
      @Nullable String schemeType, @Nullable android.media.DrmInitData drmInitData) {
    if (drmInitData == null) {
      return null;
    }
    SchemeData[] schemeDatas = new SchemeData[drmInitData.getSchemeInitDataCount()];
    for (int i = 0; i < schemeDatas.length; i++) {
      SchemeInitData schemeInitData = drmInitData.getSchemeInitDataAt(i);
      schemeDatas[i] =
          new SchemeData(schemeInitData.uuid, schemeInitData.mimeType, schemeInitData.data);
    }
    return new DrmInitData(schemeType, schemeDatas);
  }

  private static @SelectionFlags int getSelectionFlags(MediaFormat mediaFormat) {
    int selectionFlags = 0;
    selectionFlags |=
        getFlag(
            mediaFormat,
            /* key= */ MediaFormat.KEY_IS_AUTOSELECT,
            /* returnValueIfPresent= */ C.SELECTION_FLAG_AUTOSELECT);
    selectionFlags |=
        getFlag(
            mediaFormat,
            /* key= */ MediaFormat.KEY_IS_DEFAULT,
            /* returnValueIfPresent= */ C.SELECTION_FLAG_DEFAULT);
    selectionFlags |=
        getFlag(
            mediaFormat,
            /* key= */ MediaFormat.KEY_IS_FORCED_SUBTITLE,
            /* returnValueIfPresent= */ C.SELECTION_FLAG_FORCED);
    return selectionFlags;
  }

  private static int getFlag(MediaFormat mediaFormat, String key, int returnValueIfPresent) {
    return mediaFormat.getInteger(key, /* defaultValue= */ 0) != 0 ? returnValueIfPresent : 0;
  }

  private static List<byte[]> getInitializationData(MediaFormat mediaFormat) {
    ArrayList<byte[]> initData = new ArrayList<>();
    int i = 0;
    while (true) {
      @Nullable ByteBuffer byteBuffer = mediaFormat.getByteBuffer("csd-" + i++);
      if (byteBuffer == null) {
        break;
      }
      initData.add(getArray(byteBuffer));
    }
    return initData;
  }

  @Nullable
  private static ColorInfo getColorInfo(MediaFormat mediaFormat) {
    @Nullable
    ByteBuffer hdrStaticInfoByteBuffer = mediaFormat.getByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO);
    @Nullable
    byte[] hdrStaticInfo =
        hdrStaticInfoByteBuffer != null ? getArray(hdrStaticInfoByteBuffer) : null;
    int colorTransfer =
        mediaFormat.getInteger(MediaFormat.KEY_COLOR_TRANSFER, /* defaultValue= */ Format.NO_VALUE);
    int colorRange =
        mediaFormat.getInteger(MediaFormat.KEY_COLOR_RANGE, /* defaultValue= */ Format.NO_VALUE);
    int colorStandard =
        mediaFormat.getInteger(MediaFormat.KEY_COLOR_STANDARD, /* defaultValue= */ Format.NO_VALUE);

    if (hdrStaticInfo != null
        || colorTransfer != Format.NO_VALUE
        || colorRange != Format.NO_VALUE
        || colorStandard != Format.NO_VALUE) {
      return new ColorInfo(colorStandard, colorRange, colorTransfer, hdrStaticInfo);
    }
    return null;
  }

  private static byte[] getArray(ByteBuffer byteBuffer) {
    byte[] array = new byte[byteBuffer.remaining()];
    byteBuffer.get(array);
    return array;
  }

  private static String getMimeType(String parserName) {
    switch (parserName) {
      case PARSER_NAME_MATROSKA:
        return MimeTypes.VIDEO_WEBM;
      case PARSER_NAME_FMP4:
      case PARSER_NAME_MP4:
        return MimeTypes.VIDEO_MP4;
      case PARSER_NAME_MP3:
        return MimeTypes.AUDIO_MPEG;
      case PARSER_NAME_ADTS:
        return MimeTypes.AUDIO_AAC;
      case PARSER_NAME_AC3:
        return MimeTypes.AUDIO_AC3;
      case PARSER_NAME_TS:
        return MimeTypes.VIDEO_MP2T;
      case PARSER_NAME_FLV:
        return MimeTypes.VIDEO_FLV;
      case PARSER_NAME_OGG:
        return MimeTypes.AUDIO_OGG;
      case PARSER_NAME_PS:
        return MimeTypes.VIDEO_PS;
      case PARSER_NAME_WAV:
        return MimeTypes.AUDIO_RAW;
      case PARSER_NAME_AMR:
        return MimeTypes.AUDIO_AMR;
      case PARSER_NAME_AC4:
        return MimeTypes.AUDIO_AC4;
      case PARSER_NAME_FLAC:
        return MimeTypes.AUDIO_FLAC;
      default:
        throw new IllegalArgumentException("Illegal parser name: " + parserName);
    }
  }

  private static final class SeekMapAdapter implements SeekMap {

    private final MediaParser.SeekMap adaptedSeekMap;

    public SeekMapAdapter(MediaParser.SeekMap adaptedSeekMap) {
      this.adaptedSeekMap = adaptedSeekMap;
    }

    @Override
    public boolean isSeekable() {
      return adaptedSeekMap.isSeekable();
    }

    @Override
    public long getDurationUs() {
      long durationMicros = adaptedSeekMap.getDurationMicros();
      return durationMicros != MediaParser.SeekMap.UNKNOWN_DURATION ? durationMicros : C.TIME_UNSET;
    }

    @Override
    @SuppressWarnings("ReferenceEquality")
    public SeekPoints getSeekPoints(long timeUs) {
      Pair<MediaParser.SeekPoint, MediaParser.SeekPoint> seekPoints =
          adaptedSeekMap.getSeekPoints(timeUs);
      SeekPoints exoPlayerSeekPoints;
      if (seekPoints.first == seekPoints.second) {
        exoPlayerSeekPoints = new SeekPoints(asExoPlayerSeekPoint(seekPoints.first));
      } else {
        exoPlayerSeekPoints =
            new SeekPoints(
                asExoPlayerSeekPoint(seekPoints.first), asExoPlayerSeekPoint(seekPoints.second));
      }
      return exoPlayerSeekPoints;
    }

    private static SeekPoint asExoPlayerSeekPoint(MediaParser.SeekPoint seekPoint) {
      return new SeekPoint(seekPoint.timeMicros, seekPoint.position);
    }
  }

  private static final class DataReaderAdapter implements DataReader {

    @Nullable public MediaParser.InputReader input;

    @Override
    public int read(byte[] buffer, int offset, int length) throws IOException {
      return Util.castNonNull(input).read(buffer, offset, length);
    }
  }
}