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