/* * Copyright (C) 2019 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 android.media; import android.annotation.CheckResult; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringDef; import android.media.MediaCodec.CryptoInfo; import android.media.metrics.LogSessionId; import android.os.Build; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.util.SparseArray; import androidx.annotation.RequiresApi; import com.android.modules.utils.build.SdkLevel; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.amr.AmrExtractor; import com.google.android.exoplayer2.extractor.flac.FlacExtractor; import com.google.android.exoplayer2.extractor.flv.FlvExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; import com.google.android.exoplayer2.extractor.ogg.OggExtractor; import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; import com.google.android.exoplayer2.extractor.ts.Ac4Extractor; import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; import com.google.android.exoplayer2.extractor.ts.PsExtractor; import com.google.android.exoplayer2.extractor.ts.TsExtractor; import com.google.android.exoplayer2.extractor.wav.WavExtractor; import com.google.android.exoplayer2.upstream.DataReader; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.video.ColorInfo; import com.google.common.base.Ascii; import java.io.EOFException; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; import java.util.function.Function; /** * Parses media container formats and extracts contained media samples and metadata. * *
This class provides access to a battery of low-level media container parsers. Each instance of * this class is associated to a specific media parser implementation which is suitable for * extraction from a specific media container format. The media parser implementation assignment * depends on the factory method (see {@link #create} and {@link #createByName}) used to create the * instance. * *
Users must implement the following to use this class. * *
The following code snippet includes a usage example: * *
* MyOutputConsumer myOutputConsumer = new MyOutputConsumer(); * MyInputReader myInputReader = new MyInputReader("www.example.com"); * MediaParser mediaParser = MediaParser.create(myOutputConsumer); * * while (mediaParser.advance(myInputReader)) {} * * mediaParser.release(); * mediaParser = null; ** *
The following code snippet provides a rudimentary {@link OutputConsumer} sample implementation * which extracts and publishes all video samples: * *
* class VideoOutputConsumer implements MediaParser.OutputConsumer { * * private byte[] sampleDataBuffer = new byte[4096]; * private byte[] discardedDataBuffer = new byte[4096]; * private int videoTrackIndex = -1; * private int bytesWrittenCount = 0; * * @Override * public void onSeekMapFound(int i, @NonNull MediaFormat mediaFormat) { * // Do nothing. * } * * @Override * public void onTrackDataFound(int i, @NonNull TrackData trackData) { * MediaFormat mediaFormat = trackData.mediaFormat; * if (videoTrackIndex == -1 && * mediaFormat * .getString(MediaFormat.KEY_MIME, /* defaultValue= */ "") * .startsWith("video/")) { * videoTrackIndex = i; * } * } * * @Override * public void onSampleDataFound(int trackIndex, @NonNull InputReader inputReader) * throws IOException { * int numberOfBytesToRead = (int) inputReader.getLength(); * if (videoTrackIndex != trackIndex) { * // Discard contents. * inputReader.read( * discardedDataBuffer, * /* offset= */ 0, * Math.min(discardDataBuffer.length, numberOfBytesToRead)); * } else { * ensureSpaceInBuffer(numberOfBytesToRead); * int bytesRead = inputReader.read( * sampleDataBuffer, bytesWrittenCount, numberOfBytesToRead); * bytesWrittenCount += bytesRead; * } * } * * @Override * public void onSampleCompleted( * int trackIndex, * long timeMicros, * int flags, * int size, * int offset, * @Nullable CryptoInfo cryptoData) { * if (videoTrackIndex != trackIndex) { * return; // It's not the video track. Ignore. * } * byte[] sampleData = new byte[size]; * int sampleStartOffset = bytesWrittenCount - size - offset; * System.arraycopy( * sampleDataBuffer, * sampleStartOffset, * sampleData, * /* destPos= */ 0, * size); * // Place trailing bytes at the start of the buffer. * System.arraycopy( * sampleDataBuffer, * bytesWrittenCount - offset, * sampleDataBuffer, * /* destPos= */ 0, * /* size= */ offset); * bytesWrittenCount = bytesWrittenCount - offset; * publishSample(sampleData, timeMicros, flags); * } * * private void ensureSpaceInBuffer(int numberOfBytesToRead) { * int requiredLength = bytesWrittenCount + numberOfBytesToRead; * if (requiredLength > sampleDataBuffer.length) { * sampleDataBuffer = Arrays.copyOf(sampleDataBuffer, requiredLength); * } * } * * } * **/ @RequiresApi(Build.VERSION_CODES.R) public final class MediaParser { /** * Maps seek positions to {@link SeekPoint SeekPoints} in the stream. * *
A {@link SeekPoint} is a position in the stream from which a player may successfully start * playing media samples. */ public static final class SeekMap { /** Returned by {@link #getDurationMicros()} when the duration is unknown. */ public static final int UNKNOWN_DURATION = Integer.MIN_VALUE; /** * For each {@link #getSeekPoints} call, returns a single {@link SeekPoint} whose {@link * SeekPoint#timeMicros} matches the requested timestamp, and whose {@link * SeekPoint#position} is 0. * * @hide */ public static final SeekMap DUMMY = new SeekMap(new DummyExoPlayerSeekMap()); private final com.google.android.exoplayer2.extractor.SeekMap mExoPlayerSeekMap; private SeekMap(com.google.android.exoplayer2.extractor.SeekMap exoplayerSeekMap) { mExoPlayerSeekMap = exoplayerSeekMap; } /** Returns whether seeking is supported. */ public boolean isSeekable() { return mExoPlayerSeekMap.isSeekable(); } /** * Returns the duration of the stream in microseconds or {@link #UNKNOWN_DURATION} if the * duration is unknown. */ public long getDurationMicros() { long durationUs = mExoPlayerSeekMap.getDurationUs(); return durationUs != C.TIME_UNSET ? durationUs : UNKNOWN_DURATION; } /** * Obtains {@link SeekPoint SeekPoints} for the specified seek time in microseconds. * *
{@code getSeekPoints(timeMicros).first} contains the latest seek point for samples * with timestamp equal to or smaller than {@code timeMicros}. * *
{@code getSeekPoints(timeMicros).second} contains the earliest seek point for samples
* with timestamp equal to or greater than {@code timeMicros}. If a seek point exists for
* {@code timeMicros}, the returned pair will contain the same {@link SeekPoint} twice.
*
* @param timeMicros A seek time in microseconds.
* @return The corresponding {@link SeekPoint SeekPoints}.
*/
@NonNull
public Pair This method blocks until at least one byte is read, the end of input is detected, or
* an exception is thrown. The read position advances to the first unread byte.
*
* @param buffer The buffer into which the read data should be stored.
* @param offset The start offset into {@code buffer} at which data should be written.
* @param readLength The maximum number of bytes to read.
* @return The non-zero number of bytes read, or -1 if no data is available because the end
* of the input has been reached.
* @throws java.io.IOException If an error occurs reading from the source.
*/
int read(@NonNull byte[] buffer, int offset, int readLength) throws IOException;
/** Returns the current read position (byte offset) in the stream. */
long getPosition();
/** Returns the length of the input in bytes, or -1 if the length is unknown. */
long getLength();
}
/** {@link InputReader} that allows setting the read position. */
public interface SeekableInputReader extends InputReader {
/**
* Sets the read position at the given {@code position}.
*
* {@link #advance} will immediately return after calling this method.
*
* @param position The position to seek to, in bytes.
*/
void seekToPosition(long position);
}
/** Receives extracted media sample data and metadata from {@link MediaParser}. */
public interface OutputConsumer {
/**
* Called when a {@link SeekMap} has been extracted from the stream.
*
* This method is called at least once before any samples are {@link #onSampleCompleted
* complete}. May be called multiple times after that in order to add {@link SeekPoint
* SeekPoints}.
*
* @param seekMap The extracted {@link SeekMap}.
*/
void onSeekMapFound(@NonNull SeekMap seekMap);
/**
* Called when the number of tracks is found.
*
* @param numberOfTracks The number of tracks in the stream.
*/
void onTrackCountFound(int numberOfTracks);
/**
* Called when new {@link TrackData} is found in the stream.
*
* @param trackIndex The index of the track for which the {@link TrackData} was extracted.
* @param trackData The extracted {@link TrackData}.
*/
void onTrackDataFound(int trackIndex, @NonNull TrackData trackData);
/**
* Called when sample data is found in the stream.
*
* If the invocation of this method returns before the entire {@code inputReader} {@link
* InputReader#getLength() length} is consumed, the method will be called again for the
* implementer to read the remaining data. Implementers should surface any thrown {@link
* IOException} caused by reading from {@code input}.
*
* @param trackIndex The index of the track to which the sample data corresponds.
* @param inputReader The {@link InputReader} from which to read the data.
* @throws IOException If an exception occurs while reading from {@code inputReader}.
*/
void onSampleDataFound(int trackIndex, @NonNull InputReader inputReader) throws IOException;
/**
* Called once all the data of a sample has been passed to {@link #onSampleDataFound}.
*
* Includes sample metadata, like presentation timestamp and flags.
*
* @param trackIndex The index of the track to which the sample corresponds.
* @param timeMicros The media timestamp associated with the sample, in microseconds.
* @param flags Flags associated with the sample. See the {@code SAMPLE_FLAG_*} constants.
* @param size The size of the sample data, in bytes.
* @param offset The number of bytes that have been consumed by {@code
* onSampleDataFound(int, MediaParser.InputReader)} for the specified track, since the
* last byte belonging to the sample whose metadata is being passed.
* @param cryptoInfo Encryption data required to decrypt the sample. May be null for
* unencrypted samples. Implementors should treat any output {@link CryptoInfo}
* instances as immutable. MediaParser will not modify any output {@code cryptoInfos}
* and implementors should not modify them either.
*/
void onSampleCompleted(
int trackIndex,
long timeMicros,
@SampleFlags int flags,
int size,
int offset,
@Nullable CryptoInfo cryptoInfo);
}
/**
* Thrown if all parser implementations provided to {@link #create} failed to sniff the input
* content.
*/
public static final class UnrecognizedInputFormatException extends IOException {
/**
* Creates a new instance which signals that the parsers with the given names failed to
* parse the input.
*/
@NonNull
@CheckResult
private static UnrecognizedInputFormatException createForExtractors(
@NonNull String... extractorNames) {
StringBuilder builder = new StringBuilder();
builder.append("None of the available parsers ( ");
builder.append(extractorNames[0]);
for (int i = 1; i < extractorNames.length; i++) {
builder.append(", ");
builder.append(extractorNames[i]);
}
builder.append(") could read the stream.");
return new UnrecognizedInputFormatException(builder.toString());
}
private UnrecognizedInputFormatException(String extractorNames) {
super(extractorNames);
}
}
/** Thrown when an error occurs while parsing a media stream. */
public static final class ParsingException extends IOException {
private ParsingException(ParserException cause) {
super(cause);
}
}
// Sample flags.
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {
SAMPLE_FLAG_KEY_FRAME,
SAMPLE_FLAG_HAS_SUPPLEMENTAL_DATA,
SAMPLE_FLAG_LAST_SAMPLE,
SAMPLE_FLAG_ENCRYPTED,
SAMPLE_FLAG_DECODE_ONLY
})
public @interface SampleFlags {}
/** Indicates that the sample holds a synchronization sample. */
public static final int SAMPLE_FLAG_KEY_FRAME = MediaCodec.BUFFER_FLAG_KEY_FRAME;
/**
* Indicates that the sample has supplemental data.
*
* Samples will not have this flag set unless the {@code
* "android.media.mediaparser.includeSupplementalData"} parameter is set to {@code true} via
* {@link #setParameter}.
*
* Samples with supplemental data have the following sample data format:
*
* If this flag is enabled and the cues element occurs after the first cluster, then the
* media is treated as unseekable.
*/
public static final String PARAMETER_MATROSKA_DISABLE_CUES_SEEKING =
"android.media.mediaparser.matroska.disableCuesSeeking";
/**
* Sets whether the ID3 track should be disabled for MP3. {@code boolean} expected. Default
* value is {@code false}.
*/
public static final String PARAMETER_MP3_DISABLE_ID3 =
"android.media.mediaparser.mp3.disableId3";
/**
* Sets whether constant bitrate seeking should be enabled for MP3. {@code boolean} expected.
* Default value is {@code false}.
*/
public static final String PARAMETER_MP3_ENABLE_CBR_SEEKING =
"android.media.mediaparser.mp3.enableCbrSeeking";
/**
* Sets whether MP3 parsing should generate a time-to-byte mapping. {@code boolean} expected.
* Default value is {@code false}.
*
* Enabling this flag may require to scan a significant portion of the file to compute a seek
* point. Therefore, it should only be used if:
*
* The operation modes alter the way TS behaves so that it can handle certain kinds of
* commonly-occurring malformed media.
*
* This flag should be left disabled if the stream contains access units delimiters in order
* to avoid unnecessary computational costs.
*/
public static final String PARAMETER_TS_DETECT_ACCESS_UNITS =
"android.media.mediaparser.ts.ignoreDetectAccessUnits";
/**
* Sets whether TS parsing should handle HDMV DTS audio streams. {@code boolean} expected.
* Default value is {@code false}.
*
* Enabling this flag will disable the detection of SCTE subtitles.
*/
public static final String PARAMETER_TS_ENABLE_HDMV_DTS_AUDIO_STREAMS =
"android.media.mediaparser.ts.enableHdmvDtsAudioStreams";
/**
* Sets whether encryption data should be sent in-band with the sample data, as per {@link
* OutputConsumer#onSampleDataFound}. {@code boolean} expected. Default value is {@code false}.
*
* If this parameter is set, encrypted samples' data will be prefixed with the encryption
* information bytes. The format for in-band encryption information is:
*
* When set to true, sample timestamps will not be offset to start from zero, and the media
* provided timestamps will be used instead. For example, transport stream sample timestamps
* will not be converted to a zero-based timebase.
*
* @hide
*/
public static final String PARAMETER_IGNORE_TIMESTAMP_OFFSET =
"android.media.mediaparser.ignoreTimestampOffset";
/**
* Sets whether each track type should be eagerly exposed. {@code boolean} expected. Default
* value is {@code false}.
*
* When set to true, each track type will be eagerly exposed through a call to {@link
* OutputConsumer#onTrackDataFound} containing a single-value {@link MediaFormat}. The key for
* the track type is {@code "track-type-string"}, and the possible values are {@code "video"},
* {@code "audio"}, {@code "text"}, {@code "metadata"}, and {@code "unknown"}.
*
* @hide
*/
public static final String PARAMETER_EAGERLY_EXPOSE_TRACKTYPE =
"android.media.mediaparser.eagerlyExposeTrackType";
/**
* Sets whether a dummy {@link SeekMap} should be exposed before starting extraction. {@code
* boolean} expected. Default value is {@code false}.
*
* For each {@link SeekMap#getSeekPoints} call, the dummy {@link SeekMap} returns a single
* {@link SeekPoint} whose {@link SeekPoint#timeMicros} matches the requested timestamp, and
* whose {@link SeekPoint#position} is 0.
*
* @hide
*/
public static final String PARAMETER_EXPOSE_DUMMY_SEEKMAP =
"android.media.mediaparser.exposeDummySeekMap";
/**
* Sets whether chunk indices available in the extracted media should be exposed as {@link
* MediaFormat MediaFormats}. {@code boolean} expected. Default value is {@link false}.
*
* When set to true, any information about media segmentation will be exposed as a {@link
* MediaFormat} (with track index 0) containing four {@link ByteBuffer} elements under the
* following keys:
*
* Expected keys in the {@link MediaFormat} are:
*
* {@link MediaFormat#KEY_MIME}: Determine the type of captions (for example,
* application/cea-608). Mandatory.
* {@link MediaFormat#KEY_CAPTION_SERVICE_NUMBER}: Determine the channel on which the
* captions are transmitted. Optional.
* When {@code false}, any present in-band caption services information will override the
* values associated with {@link #PARAMETER_EXPOSE_CAPTION_FORMATS}.
*
* @hide
*/
public static final String PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS =
"android.media.mediaParser.overrideInBandCaptionDeclarations";
/**
* Sets whether a track for EMSG events should be exposed in case of parsing a container that
* supports them. {@code boolean} expected. Default value is {@link false}.
*
* @hide
*/
public static final String PARAMETER_EXPOSE_EMSG_TRACK =
"android.media.mediaParser.exposeEmsgTrack";
// Private constants.
private static final String TAG = "MediaParser";
private static final String JNI_LIBRARY_NAME = "mediaparser-jni";
private static final Map A parser supports a {@link MediaFormat} if the mime type associated with {@link
* MediaFormat#KEY_MIME} corresponds to the supported container format.
*
* @param mediaFormat The {@link MediaFormat} to check support for.
* @return The parser names that support the given {@code mediaFormat}, or the list of all
* parsers available if no container specific format information is provided.
*/
@NonNull
@ParserName
public static List Must be called before the first call to {@link #advance}.
*
* @param parameterName The name of the parameter to set. See {@code PARAMETER_*} constants for
* documentation on possible values.
* @param value The value to set for the given {@code parameterName}. See {@code PARAMETER_*}
* constants for documentation on the expected types.
* @return This instance, for convenience.
* @throws IllegalStateException If called after calling {@link #advance} on the same instance.
*/
@NonNull
public MediaParser setParameter(
@NonNull @ParameterName String parameterName, @NonNull Object value) {
if (mExtractor != null) {
throw new IllegalStateException(
"setParameters() must be called before the first advance() call.");
}
Class expectedType = EXPECTED_TYPE_BY_PARAMETER_NAME.get(parameterName);
// Ignore parameter names that are not contained in the map, in case the client is passing
// a parameter that is being added in a future version of this library.
if (expectedType != null && !expectedType.isInstance(value)) {
throw new IllegalArgumentException(
parameterName
+ " expects a "
+ expectedType.getSimpleName()
+ " but a "
+ value.getClass().getSimpleName()
+ " was passed.");
}
if (PARAMETER_TS_MODE.equals(parameterName)
&& !TS_MODE_SINGLE_PMT.equals(value)
&& !TS_MODE_HLS.equals(value)
&& !TS_MODE_MULTI_PMT.equals(value)) {
throw new IllegalArgumentException(PARAMETER_TS_MODE + " does not accept: " + value);
}
if (PARAMETER_IN_BAND_CRYPTO_INFO.equals(parameterName)) {
mInBandCryptoInfo = (boolean) value;
}
if (PARAMETER_INCLUDE_SUPPLEMENTAL_DATA.equals(parameterName)) {
mIncludeSupplementalData = (boolean) value;
}
if (PARAMETER_IGNORE_TIMESTAMP_OFFSET.equals(parameterName)) {
mIgnoreTimestampOffset = (boolean) value;
}
if (PARAMETER_EAGERLY_EXPOSE_TRACKTYPE.equals(parameterName)) {
mEagerlyExposeTrackType = (boolean) value;
}
if (PARAMETER_EXPOSE_DUMMY_SEEKMAP.equals(parameterName)) {
mExposeDummySeekMap = (boolean) value;
}
if (PARAMETER_EXPOSE_CHUNK_INDEX_AS_MEDIA_FORMAT.equals(parameterName)) {
mExposeChunkIndexAsMediaFormat = (boolean) value;
}
if (PARAMETER_EXPOSE_CAPTION_FORMATS.equals(parameterName)) {
setMuxedCaptionFormats((List If this instance was creating using {@link #createByName}, the provided name is returned.
* If this instance was created using {@link #create}, this method will return {@link
* #PARSER_NAME_UNKNOWN} until the first call to {@link #advance}, after which the name of the
* backing parser implementation is returned.
*
* @return The name of the backing parser implementation, or null if the backing parser
* implementation has not yet been selected.
*/
@NonNull
@ParserName
public String getParserName() {
return mParserName;
}
/**
* Makes progress in the extraction of the input media stream, unless the end of the input has
* been reached.
*
* This method will block until some progress has been made.
*
* If this instance was created using {@link #create}, the first call to this method will
* sniff the content using the selected parser implementations.
*
* @param seekableInputReader The {@link SeekableInputReader} from which to obtain the media
* container data.
* @return Whether there is any data left to extract. Returns false if the end of input has been
* reached.
* @throws IOException If an error occurs while reading from the {@link SeekableInputReader}.
* @throws UnrecognizedInputFormatException If the format cannot be recognized by any of the
* underlying parser implementations.
*/
public boolean advance(@NonNull SeekableInputReader seekableInputReader) throws IOException {
if (mExtractorInput == null) {
// TODO: For efficiency, the same implementation should be used, by providing a
// clearBuffers() method, or similar.
long resourceLength = seekableInputReader.getLength();
if (mResourceByteCount == 0) {
// For resource byte count metric collection, we only take into account the length
// of the first provided input reader.
mResourceByteCount = resourceLength;
}
mExtractorInput =
new DefaultExtractorInput(
mExoDataReader, seekableInputReader.getPosition(), resourceLength);
}
mExoDataReader.mInputReader = seekableInputReader;
if (mExtractor == null) {
mPendingExtractorInit = true;
if (!mParserName.equals(PARSER_NAME_UNKNOWN)) {
mExtractor = createExtractor(mParserName);
} else {
for (String parserName : mParserNamesPool) {
Extractor extractor = createExtractor(parserName);
try {
if (extractor.sniff(mExtractorInput)) {
mParserName = parserName;
mExtractor = extractor;
mPendingExtractorInit = true;
break;
}
} catch (EOFException e) {
// Do nothing.
} finally {
mExtractorInput.resetPeekPosition();
}
}
if (mExtractor == null) {
UnrecognizedInputFormatException exception =
UnrecognizedInputFormatException.createForExtractors(mParserNamesPool);
mLastObservedExceptionName = exception.getClass().getName();
throw exception;
}
return true;
}
}
if (mPendingExtractorInit) {
if (mExposeDummySeekMap) {
// We propagate the dummy seek map before initializing the extractor, in case the
// extractor initialization outputs a seek map.
mOutputConsumer.onSeekMapFound(SeekMap.DUMMY);
}
mExtractor.init(new ExtractorOutputAdapter());
mPendingExtractorInit = false;
// We return after initialization to allow clients use any output information before
// starting actual extraction.
return true;
}
if (isPendingSeek()) {
mExtractor.seek(mPendingSeekPosition, mPendingSeekTimeMicros);
removePendingSeek();
}
mPositionHolder.position = seekableInputReader.getPosition();
int result;
try {
result = mExtractor.read(mExtractorInput, mPositionHolder);
} catch (Exception e) {
mLastObservedExceptionName = e.getClass().getName();
if (e instanceof ParserException) {
throw new ParsingException((ParserException) e);
} else {
throw e;
}
}
if (result == Extractor.RESULT_END_OF_INPUT) {
mExtractorInput = null;
return false;
}
if (result == Extractor.RESULT_SEEK) {
mExtractorInput = null;
seekableInputReader.seekToPosition(mPositionHolder.position);
}
return true;
}
/**
* Seeks within the media container being extracted.
*
* {@link SeekPoint SeekPoints} can be obtained from the {@link SeekMap} passed to {@link
* OutputConsumer#onSeekMapFound(SeekMap)}.
*
* Following a call to this method, the {@link InputReader} passed to the next invocation of
* {@link #advance} must provide data starting from {@link SeekPoint#position} in the stream.
*
* @param seekPoint The {@link SeekPoint} to seek to.
*/
public void seek(@NonNull SeekPoint seekPoint) {
if (mExtractor == null) {
mPendingSeekPosition = seekPoint.position;
mPendingSeekTimeMicros = seekPoint.timeMicros;
} else {
mExtractor.seek(seekPoint.position, seekPoint.timeMicros);
}
}
/**
* Releases any acquired resources.
*
* After calling this method, this instance becomes unusable and no other methods should be
* invoked.
*/
public void release() {
mExtractorInput = null;
mExtractor = null;
if (mReleased) {
// Nothing to do.
return;
}
mReleased = true;
String trackMimeTypes = buildMediaMetricsString(format -> format.sampleMimeType);
String trackCodecs = buildMediaMetricsString(format -> format.codecs);
int videoWidth = -1;
int videoHeight = -1;
for (int i = 0; i < mTrackFormats.size(); i++) {
Format format = mTrackFormats.valueAt(i);
if (format.width != Format.NO_VALUE && format.height != Format.NO_VALUE) {
videoWidth = format.width;
videoHeight = format.height;
break;
}
}
String alteredParameters =
String.join(
MEDIAMETRICS_ELEMENT_SEPARATOR,
mParserParameters.keySet().toArray(new String[0]));
alteredParameters =
alteredParameters.substring(
0,
Math.min(
alteredParameters.length(),
MEDIAMETRICS_PARAMETER_LIST_MAX_LENGTH));
nativeSubmitMetrics(
SdkLevel.isAtLeastS() ? getLogSessionIdStringV31() : "",
mParserName,
mCreatedByName,
String.join(MEDIAMETRICS_ELEMENT_SEPARATOR, mParserNamesPool),
mLastObservedExceptionName,
addDither(mResourceByteCount),
addDither(mDurationMillis),
trackMimeTypes,
trackCodecs,
alteredParameters,
videoWidth,
videoHeight);
}
@RequiresApi(31)
public void setLogSessionId(@NonNull LogSessionId logSessionId) {
this.mLogSessionId = Objects.requireNonNull(logSessionId);
}
@RequiresApi(31)
@NonNull
public LogSessionId getLogSessionId() {
return mLogSessionId != null ? mLogSessionId : LogSessionId.LOG_SESSION_ID_NONE;
}
// Private methods.
private MediaParser(
OutputConsumer outputConsumer, boolean createdByName, String... parserNamesPool) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
throw new UnsupportedOperationException("Android version must be R or greater.");
}
mParserParameters = new HashMap<>();
mOutputConsumer = outputConsumer;
mParserNamesPool = parserNamesPool;
mCreatedByName = createdByName;
mParserName = createdByName ? parserNamesPool[0] : PARSER_NAME_UNKNOWN;
mPositionHolder = new PositionHolder();
mExoDataReader = new InputReadingDataReader();
removePendingSeek();
mScratchDataReaderAdapter = new DataReaderAdapter();
mScratchParsableByteArrayAdapter = new ParsableByteArrayAdapter();
mSchemeInitDataConstructor = getSchemeInitDataConstructor();
mMuxedCaptionFormats = new ArrayList<>();
// MediaMetrics.
mTrackFormats = new SparseArray<>();
mLastObservedExceptionName = "";
mDurationMillis = -1;
}
private String buildMediaMetricsString(Function
*
*/
public static final int SAMPLE_FLAG_HAS_SUPPLEMENTAL_DATA = 1 << 28;
/** Indicates that the sample is known to contain the last media sample of the stream. */
public static final int SAMPLE_FLAG_LAST_SAMPLE = 1 << 29;
/** Indicates that the sample is (at least partially) encrypted. */
public static final int SAMPLE_FLAG_ENCRYPTED = 1 << 30;
/** Indicates that the sample should be decoded but not rendered. */
public static final int SAMPLE_FLAG_DECODE_ONLY = 1 << 31;
// Parser implementation names.
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@StringDef(
prefix = {"PARSER_NAME_"},
value = {
PARSER_NAME_UNKNOWN,
PARSER_NAME_MATROSKA,
PARSER_NAME_FMP4,
PARSER_NAME_MP4,
PARSER_NAME_MP3,
PARSER_NAME_ADTS,
PARSER_NAME_AC3,
PARSER_NAME_TS,
PARSER_NAME_FLV,
PARSER_NAME_OGG,
PARSER_NAME_PS,
PARSER_NAME_WAV,
PARSER_NAME_AMR,
PARSER_NAME_AC4,
PARSER_NAME_FLAC
})
public @interface ParserName {}
/** Parser name returned by {@link #getParserName()} when no parser has been selected yet. */
public static final String PARSER_NAME_UNKNOWN = "android.media.mediaparser.UNKNOWN";
/**
* Parser for the Matroska container format, as defined in the spec.
*/
public static final String PARSER_NAME_MATROSKA = "android.media.mediaparser.MatroskaParser";
/**
* Parser for fragmented files using the MP4 container format, as defined in ISO/IEC 14496-12.
*/
public static final String PARSER_NAME_FMP4 = "android.media.mediaparser.FragmentedMp4Parser";
/**
* Parser for non-fragmented files using the MP4 container format, as defined in ISO/IEC
* 14496-12.
*/
public static final String PARSER_NAME_MP4 = "android.media.mediaparser.Mp4Parser";
/** Parser for the MP3 container format, as defined in ISO/IEC 11172-3. */
public static final String PARSER_NAME_MP3 = "android.media.mediaparser.Mp3Parser";
/** Parser for the ADTS container format, as defined in ISO/IEC 13818-7. */
public static final String PARSER_NAME_ADTS = "android.media.mediaparser.AdtsParser";
/**
* Parser for the AC-3 container format, as defined in Digital Audio Compression Standard
* (AC-3).
*/
public static final String PARSER_NAME_AC3 = "android.media.mediaparser.Ac3Parser";
/** Parser for the TS container format, as defined in ISO/IEC 13818-1. */
public static final String PARSER_NAME_TS = "android.media.mediaparser.TsParser";
/**
* Parser for the FLV container format, as defined in Adobe Flash Video File Format
* Specification.
*/
public static final String PARSER_NAME_FLV = "android.media.mediaparser.FlvParser";
/** Parser for the OGG container format, as defined in RFC 3533. */
public static final String PARSER_NAME_OGG = "android.media.mediaparser.OggParser";
/** Parser for the PS container format, as defined in ISO/IEC 11172-1. */
public static final String PARSER_NAME_PS = "android.media.mediaparser.PsParser";
/**
* Parser for the WAV container format, as defined in Multimedia Programming Interface and Data
* Specifications.
*/
public static final String PARSER_NAME_WAV = "android.media.mediaparser.WavParser";
/** Parser for the AMR container format, as defined in RFC 4867. */
public static final String PARSER_NAME_AMR = "android.media.mediaparser.AmrParser";
/**
* Parser for the AC-4 container format, as defined by Dolby AC-4: Audio delivery for
* Next-Generation Entertainment Services.
*/
public static final String PARSER_NAME_AC4 = "android.media.mediaparser.Ac4Parser";
/**
* Parser for the FLAC container format, as defined in the spec.
*/
public static final String PARSER_NAME_FLAC = "android.media.mediaparser.FlacParser";
// MediaParser parameters.
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@StringDef(
prefix = {"PARAMETER_"},
value = {
PARAMETER_ADTS_ENABLE_CBR_SEEKING,
PARAMETER_AMR_ENABLE_CBR_SEEKING,
PARAMETER_FLAC_DISABLE_ID3,
PARAMETER_MP4_IGNORE_EDIT_LISTS,
PARAMETER_MP4_IGNORE_TFDT_BOX,
PARAMETER_MP4_TREAT_VIDEO_FRAMES_AS_KEYFRAMES,
PARAMETER_MATROSKA_DISABLE_CUES_SEEKING,
PARAMETER_MP3_DISABLE_ID3,
PARAMETER_MP3_ENABLE_CBR_SEEKING,
PARAMETER_MP3_ENABLE_INDEX_SEEKING,
PARAMETER_TS_MODE,
PARAMETER_TS_ALLOW_NON_IDR_AVC_KEYFRAMES,
PARAMETER_TS_IGNORE_AAC_STREAM,
PARAMETER_TS_IGNORE_AVC_STREAM,
PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM,
PARAMETER_TS_DETECT_ACCESS_UNITS,
PARAMETER_TS_ENABLE_HDMV_DTS_AUDIO_STREAMS,
PARAMETER_IN_BAND_CRYPTO_INFO,
PARAMETER_INCLUDE_SUPPLEMENTAL_DATA
})
public @interface ParameterName {}
/**
* Sets whether constant bitrate seeking should be enabled for ADTS parsing. {@code boolean}
* expected. Default value is {@code false}.
*/
public static final String PARAMETER_ADTS_ENABLE_CBR_SEEKING =
"android.media.mediaparser.adts.enableCbrSeeking";
/**
* Sets whether constant bitrate seeking should be enabled for AMR. {@code boolean} expected.
* Default value is {@code false}.
*/
public static final String PARAMETER_AMR_ENABLE_CBR_SEEKING =
"android.media.mediaparser.amr.enableCbrSeeking";
/**
* Sets whether the ID3 track should be disabled for FLAC. {@code boolean} expected. Default
* value is {@code false}.
*/
public static final String PARAMETER_FLAC_DISABLE_ID3 =
"android.media.mediaparser.flac.disableId3";
/**
* Sets whether MP4 parsing should ignore edit lists. {@code boolean} expected. Default value is
* {@code false}.
*/
public static final String PARAMETER_MP4_IGNORE_EDIT_LISTS =
"android.media.mediaparser.mp4.ignoreEditLists";
/**
* Sets whether MP4 parsing should ignore the tfdt box. {@code boolean} expected. Default value
* is {@code false}.
*/
public static final String PARAMETER_MP4_IGNORE_TFDT_BOX =
"android.media.mediaparser.mp4.ignoreTfdtBox";
/**
* Sets whether MP4 parsing should treat all video frames as key frames. {@code boolean}
* expected. Default value is {@code false}.
*/
public static final String PARAMETER_MP4_TREAT_VIDEO_FRAMES_AS_KEYFRAMES =
"android.media.mediaparser.mp4.treatVideoFramesAsKeyframes";
/**
* Sets whether Matroska parsing should avoid seeking to the cues element. {@code boolean}
* expected. Default value is {@code false}.
*
*
*
*/
public static final String PARAMETER_MP3_ENABLE_INDEX_SEEKING =
"android.media.mediaparser.mp3.enableIndexSeeking";
/**
* Sets the operation mode for TS parsing. {@code String} expected. Valid values are {@code
* "single_pmt"}, {@code "multi_pmt"}, and {@code "hls"}. Default value is {@code "single_pmt"}.
*
*
*
*/
public static final String PARAMETER_TS_MODE = "android.media.mediaparser.ts.mode";
/**
* Sets whether TS should treat samples consisting of non-IDR I slices as synchronization
* samples (key-frames). {@code boolean} expected. Default value is {@code false}.
*/
public static final String PARAMETER_TS_ALLOW_NON_IDR_AVC_KEYFRAMES =
"android.media.mediaparser.ts.allowNonIdrAvcKeyframes";
/**
* Sets whether TS parsing should ignore AAC elementary streams. {@code boolean} expected.
* Default value is {@code false}.
*/
public static final String PARAMETER_TS_IGNORE_AAC_STREAM =
"android.media.mediaparser.ts.ignoreAacStream";
/**
* Sets whether TS parsing should ignore AVC elementary streams. {@code boolean} expected.
* Default value is {@code false}.
*/
public static final String PARAMETER_TS_IGNORE_AVC_STREAM =
"android.media.mediaparser.ts.ignoreAvcStream";
/**
* Sets whether TS parsing should ignore splice information streams. {@code boolean} expected.
* Default value is {@code false}.
*/
public static final String PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM =
"android.media.mediaparser.ts.ignoreSpliceInfoStream";
/**
* Sets whether TS parsing should split AVC stream into access units based on slice headers.
* {@code boolean} expected. Default value is {@code false}.
*
*
*
*
* @hide
*/
public static final String PARAMETER_IN_BAND_CRYPTO_INFO =
"android.media.mediaparser.inBandCryptoInfo";
/**
* Sets whether supplemental data should be included as part of the sample data. {@code boolean}
* expected. Default value is {@code false}. See {@link #SAMPLE_FLAG_HAS_SUPPLEMENTAL_DATA} for
* information about the sample data format.
*
* @hide
*/
public static final String PARAMETER_INCLUDE_SUPPLEMENTAL_DATA =
"android.media.mediaparser.includeSupplementalData";
/**
* Sets whether sample timestamps may start from non-zero offsets. {@code boolean} expected.
* Default value is {@code false}.
*
*
*
*
*
*
*
*
* @hide
*/
public static final String PARAMETER_EXPOSE_CHUNK_INDEX_AS_MEDIA_FORMAT =
"android.media.mediaParser.exposeChunkIndexAsMediaFormat";
/**
* Sets a list of closed-caption {@link MediaFormat MediaFormats} that should be exposed as part
* of the extracted media. {@code List
*
*
* @hide
*/
public static final String PARAMETER_EXPOSE_CAPTION_FORMATS =
"android.media.mediaParser.exposeCaptionFormats";
/**
* Sets whether the value associated with {@link #PARAMETER_EXPOSE_CAPTION_FORMATS} should
* override any in-band caption service declarations. {@code boolean} expected. Default value is
* {@link false}.
*
*