public final class

OutputConsumerAdapterV30

extends java.lang.Object

 java.lang.Object

↳androidx.media3.exoplayer.source.mediaparser.OutputConsumerAdapterV30

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-exoplayer', version: '1.5.0-alpha01'

  • groupId: androidx.media3
  • artifactId: media3-exoplayer
  • version: 1.5.0-alpha01

Artifact androidx.media3:media3-exoplayer:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)

Overview

implementation that redirects output to an ExtractorOutput.

Summary

Constructors
publicOutputConsumerAdapterV30()

Equivalent to OutputConsumerAdapterV30(primaryTrackManifestFormat= null, primaryTrackType= C.TRACK_TYPE_NONE, expectDummySeekMap= false)

publicOutputConsumerAdapterV30(Format primaryTrackManifestFormat, int primaryTrackType, boolean expectDummySeekMap)

Creates a new instance.

Methods
public voiddisableSeeking()

Overrides future received SeekMaps with non-seekable instances.

public ChunkIndexgetChunkIndex()

Returns the most recently output ChunkIndex, or null if none has been output.

public MediaParser.SeekMapgetDummySeekMap()

Returns a dummy , or null if not available.

public FormatgetSampleFormats()

Returns the last output format for each track, or null if not all the tracks have been identified.

public <any>getSeekPoints(long seekTimeUs)

Returns the instances corresponding to the given timestamp.

public voidonSampleCompleted(int trackIndex, long timeUs, int flags, int size, int offset, MediaCodec.CryptoInfo cryptoInfo)

public voidonSampleDataFound(int trackIndex, MediaParser.InputReader sampleData)

public voidonSeekMapFound(MediaParser.SeekMap seekMap)

public voidonTrackCountFound(int numberOfTracks)

public voidonTrackDataFound(int trackIndex, TrackData trackData)

public voidsetExtractorOutput(ExtractorOutput extractorOutput)

Sets the ExtractorOutput to which MediaParser's output is directed.

public voidsetMuxedCaptionFormats(java.util.List<Format> muxedCaptionFormats)

Sets Format information associated to the caption tracks multiplexed in the media.

public voidsetSampleTimestampUpperLimitFilterUs(long sampleTimestampUpperLimitFilterUs)

Sets an upper limit for sample timestamp filtering.

public voidsetSelectedParserName(java.lang.String parserName)

Defines the container MIME type to propagate through TrackOutput.format(Format).

public voidsetTimestampAdjuster(TimestampAdjuster timestampAdjuster)

Sets a TimestampAdjuster for adjusting the timestamps of the output samples.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public OutputConsumerAdapterV30()

Equivalent to OutputConsumerAdapterV30(primaryTrackManifestFormat= null, primaryTrackType= C.TRACK_TYPE_NONE, expectDummySeekMap= false)

public OutputConsumerAdapterV30(Format primaryTrackManifestFormat, int primaryTrackType, boolean expectDummySeekMap)

Creates a new instance.

Parameters:

primaryTrackManifestFormat: The manifest-obtained format of the primary track, or null if not applicable.
primaryTrackType: The of the primary track. C.TRACK_TYPE_NONE if there is no primary track.
expectDummySeekMap: Whether the output consumer should expect an initial dummy seek map which should be exposed through OutputConsumerAdapterV30.getDummySeekMap().

Methods

public void setSampleTimestampUpperLimitFilterUs(long sampleTimestampUpperLimitFilterUs)

Sets an upper limit for sample timestamp filtering.

When set, samples with timestamps greater than sampleTimestampUpperLimitFilterUs will be discarded.

Parameters:

sampleTimestampUpperLimitFilterUs: The maximum allowed sample timestamp, or C.TIME_UNSET to remove filtering.

public void setTimestampAdjuster(TimestampAdjuster timestampAdjuster)

Sets a TimestampAdjuster for adjusting the timestamps of the output samples.

public void setExtractorOutput(ExtractorOutput extractorOutput)

Sets the ExtractorOutput to which MediaParser's output is directed.

public void setMuxedCaptionFormats(java.util.List<Format> muxedCaptionFormats)

Sets Format information associated to the caption tracks multiplexed in the media.

public void disableSeeking()

Overrides future received SeekMaps with non-seekable instances.

public MediaParser.SeekMap getDummySeekMap()

Returns a dummy , or null if not available.

the dummy returns a single whose matches the requested timestamp, and is 0.

public ChunkIndex getChunkIndex()

Returns the most recently output ChunkIndex, or null if none has been output.

public <any> getSeekPoints(long seekTimeUs)

Returns the instances corresponding to the given timestamp.

Parameters:

seekTimeUs: The timestamp in microseconds to retrieve instances for.

Returns:

The instances corresponding to the given timestamp.

public void setSelectedParserName(java.lang.String parserName)

Defines the container MIME type to propagate through TrackOutput.format(Format).

Parameters:

parserName: The name of the selected parser.

public Format getSampleFormats()

Returns the last output format for each track, or null if not all the tracks have been identified.

public void onTrackCountFound(int numberOfTracks)

public void onSeekMapFound(MediaParser.SeekMap seekMap)

public void onTrackDataFound(int trackIndex, TrackData trackData)

public void onSampleDataFound(int trackIndex, MediaParser.InputReader sampleData)

public void onSampleCompleted(int trackIndex, long timeUs, int flags, int size, int offset, MediaCodec.CryptoInfo cryptoInfo)

Source

/*
 * 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.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.MediaFormatUtil;
import androidx.media3.common.util.NullableType;
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.ExtractorOutput;
import androidx.media3.extractor.NoOpExtractorOutput;
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;

/**
 * {@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 NoOpExtractorOutput();
    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(MediaFormatUtil.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)
            .setLabels(muxedCaptionFormat.labels)
            .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(MediaFormatUtil.getArray(byteBuffer));
    }
    return initData;
  }

  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);
    }
  }
}