blob: 8bdca766e0dd5a75777328fcb655b00a079d774c [file] [log] [blame]
/*
* 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.os.Build;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
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.util.Util;
import com.google.android.exoplayer2.video.ColorInfo;
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.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Function;
/**
* Parses media container formats and extracts contained media samples and metadata.
*
* <p>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.
*
* <p>Users must implement the following to use this class.
*
* <ul>
* <li>{@link InputReader}: Provides the media container's bytes to parse.
* <li>{@link OutputConsumer}: Provides a sink for all extracted data and metadata.
* </ul>
*
* <p>The following code snippet includes a usage example:
*
* <pre>
* MyOutputConsumer myOutputConsumer = new MyOutputConsumer();
* MyInputReader myInputReader = new MyInputReader("www.example.com");
* MediaParser mediaParser = MediaParser.create(myOutputConsumer);
*
* while (mediaParser.advance(myInputReader)) {}
*
* mediaParser.release();
* mediaParser = null;
* </pre>
*
* <p>The following code snippet provides a rudimentary {@link OutputConsumer} sample implementation
* which extracts and publishes all video samples:
*
* <pre>
* 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;
*
* &#64;Override
* public void onSeekMapFound(int i, &#64;NonNull MediaFormat mediaFormat) {
* // Do nothing.
* }
*
* &#64;Override
* public void onTrackDataFound(int i, &#64;NonNull TrackData trackData) {
* MediaFormat mediaFormat = trackData.mediaFormat;
* if (videoTrackIndex == -1 &amp;&amp;
* mediaFormat
* .getString(MediaFormat.KEY_MIME, &#47;* defaultValue= *&#47; "")
* .startsWith("video/")) {
* videoTrackIndex = i;
* }
* }
*
* &#64;Override
* public void onSampleDataFound(int trackIndex, &#64;NonNull InputReader inputReader)
* throws IOException {
* int numberOfBytesToRead = (int) inputReader.getLength();
* if (videoTrackIndex != trackIndex) {
* // Discard contents.
* inputReader.read(
* discardedDataBuffer,
* &#47;* offset= *&#47; 0,
* Math.min(discardDataBuffer.length, numberOfBytesToRead));
* } else {
* ensureSpaceInBuffer(numberOfBytesToRead);
* int bytesRead = inputReader.read(
* sampleDataBuffer, bytesWrittenCount, numberOfBytesToRead);
* bytesWrittenCount += bytesRead;
* }
* }
*
* &#64;Override
* public void onSampleCompleted(
* int trackIndex,
* long timeMicros,
* int flags,
* int size,
* int offset,
* &#64;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,
* &#47;* destPos= *&#47; 0,
* size);
* // Place trailing bytes at the start of the buffer.
* System.arraycopy(
* sampleDataBuffer,
* bytesWrittenCount - offset,
* sampleDataBuffer,
* &#47;* destPos= *&#47; 0,
* &#47;* size= *&#47; offset);
* bytesWrittenCount = bytesWrittenCount - offset;
* publishSample(sampleData, timeMicros, flags);
* }
*
* private void ensureSpaceInBuffer(int numberOfBytesToRead) {
* int requiredLength = bytesWrittenCount + numberOfBytesToRead;
* if (requiredLength &gt; sampleDataBuffer.length) {
* sampleDataBuffer = Arrays.copyOf(sampleDataBuffer, requiredLength);
* }
* }
*
* }
*
* </pre>
*/
public final class MediaParser {
/**
* Maps seek positions to {@link SeekPoint SeekPoints} in the stream.
*
* <p>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.
*
* <p>{@code getSeekPoints(timeMicros).first} contains the latest seek point for samples
* with timestamp equal to or smaller than {@code timeMicros}.
*
* <p>{@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<SeekPoint, SeekPoint> getSeekPoints(long timeMicros) {
SeekPoints seekPoints = mExoPlayerSeekMap.getSeekPoints(timeMicros);
return new Pair<>(toSeekPoint(seekPoints.first), toSeekPoint(seekPoints.second));
}
}
/** Holds information associated with a track. */
public static final class TrackData {
/** Holds {@link MediaFormat} information for the track. */
@NonNull public final MediaFormat mediaFormat;
/**
* Holds {@link DrmInitData} necessary to acquire keys associated with the track, or null if
* the track has no encryption data.
*/
@Nullable public final DrmInitData drmInitData;
private TrackData(MediaFormat mediaFormat, DrmInitData drmInitData) {
this.mediaFormat = mediaFormat;
this.drmInitData = drmInitData;
}
}
/** Defines a seek point in a media stream. */
public static final class SeekPoint {
/** A {@link SeekPoint} whose time and byte offset are both set to 0. */
@NonNull public static final SeekPoint START = new SeekPoint(0, 0);
/** The time of the seek point, in microseconds. */
public final long timeMicros;
/** The byte offset of the seek point. */
public final long position;
/**
* @param timeMicros The time of the seek point, in microseconds.
* @param position The byte offset of the seek point.
*/
private SeekPoint(long timeMicros, long position) {
this.timeMicros = timeMicros;
this.position = position;
}
@Override
@NonNull
public String toString() {
return "[timeMicros=" + timeMicros + ", position=" + position + "]";
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
SeekPoint other = (SeekPoint) obj;
return timeMicros == other.timeMicros && position == other.position;
}
@Override
public int hashCode() {
int result = (int) timeMicros;
result = 31 * result + (int) position;
return result;
}
}
/** Provides input data to {@link MediaParser}. */
public interface InputReader {
/**
* Reads up to {@code readLength} bytes of data and stores them into {@code buffer},
* starting at index {@code offset}.
*
* <p>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}.
*
* <p>{@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.
*
* <p>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.
*
* <p>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}.
*
* <p>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.
*
* <p>Samples will not have this flag set unless the {@code
* "android.media.mediaparser.includeSupplementalData"} parameter is set to {@code true} via
* {@link #setParameter}.
*
* <p>Samples with supplemental data have the following sample data format:
*
* <ul>
* <li>If the {@code "android.media.mediaparser.inBandCryptoInfo"} parameter is set, all
* encryption information.
* <li>(4 bytes) {@code sample_data_size}: The size of the actual sample data, not including
* supplemental data or encryption information.
* <li>({@code sample_data_size} bytes): The media sample data.
* <li>(remaining bytes) The supplemental data.
* </ul>
*/
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 <a
* href="https://matroska.org/technical/specs/">spec</a>.
*/
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 <a
* href="https://xiph.org/flac/">spec</a>.
*/
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}.
*
* <p>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}.
*
* <p>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:
*
* <ul>
* <li>the file is small, or
* <li>the bitrate is variable (or the type of bitrate is unknown) and the seeking metadata
* provided in the file is not precise enough (or is not present).
* </ul>
*/
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"}.
*
* <p>The operation modes alter the way TS behaves so that it can handle certain kinds of
* commonly-occurring malformed media.
*
* <ul>
* <li>{@code "single_pmt"}: Only the first found PMT is parsed. Others are ignored, even if
* more PMTs are declared in the PAT.
* <li>{@code "multi_pmt"}: Behave as described in ISO/IEC 13818-1.
* <li>{@code "hls"}: Enable {@code "single_pmt"} mode, and ignore continuity counters.
* </ul>
*/
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}.
*
* <p>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}.
*
* <p>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}.
*
* <p>If this parameter is set, encrypted samples' data will be prefixed with the encryption
* information bytes. The format for in-band encryption information is:
*
* <ul>
* <li>(1 byte) {@code encryption_signal_byte}: Most significant bit signals whether the
* encryption data contains subsample encryption data. The remaining bits contain {@code
* initialization_vector_size}.
* <li>({@code initialization_vector_size} bytes) Initialization vector.
* <li>If subsample encryption data is present, as per {@code encryption_signal_byte}, the
* encryption data also contains:
* <ul>
* <li>(2 bytes) {@code subsample_encryption_data_length}.
* <li>({@code subsample_encryption_data_length * 6} bytes) Subsample encryption data
* (repeated {@code subsample_encryption_data_length} times):
* <ul>
* <li>(2 bytes) Size of a clear section in sample.
* <li>(4 bytes) Size of an encryption section in sample.
* </ul>
* </ul>
* </ul>
*
* @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}.
*
* <p>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}.
*
* <p>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}.
*
* <p>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}.
*
* <p>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:
*
* <ul>
* <li>"chunk-index-int-sizes": Contains {@code ints} representing the sizes in bytes of each
* of the media segments.
* <li>"chunk-index-long-offsets": Contains {@code longs} representing the byte offsets of
* each segment in the stream.
* <li>"chunk-index-long-us-durations": Contains {@code longs} representing the media duration
* of each segment, in microseconds.
* <li>"chunk-index-long-us-times": Contains {@code longs} representing the start time of each
* segment, in microseconds.
* </ul>
*
* @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<MediaFormat>} expected. Default value is an empty list.
*
* <p>Expected keys in the {@link MediaFormat} are:
*
* <ul>
* <p>{@link MediaFormat#KEY_MIME}: Determine the type of captions (for example,
* application/cea-608). Mandatory.
* <p>{@link MediaFormat#KEY_CAPTION_SERVICE_NUMBER}: Determine the channel on which the
* captions are transmitted. Optional.
* </ul>
*
* @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}.
*
* <p>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<String, ExtractorFactory> EXTRACTOR_FACTORIES_BY_NAME;
private static final Map<String, Class> EXPECTED_TYPE_BY_PARAMETER_NAME;
private static final String TS_MODE_SINGLE_PMT = "single_pmt";
private static final String TS_MODE_MULTI_PMT = "multi_pmt";
private static final String TS_MODE_HLS = "hls";
private static final int BYTES_PER_SUBSAMPLE_ENCRYPTION_ENTRY = 6;
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
private static final String MEDIAMETRICS_ELEMENT_SEPARATOR = "|";
private static final int MEDIAMETRICS_MAX_STRING_SIZE = 200;
private static final int MEDIAMETRICS_PARAMETER_LIST_MAX_LENGTH;
/**
* Intentional error introduced to reported metrics to prevent identification of the parsed
* media. Note: Increasing this value may cause older hostside CTS tests to fail.
*/
private static final float MEDIAMETRICS_DITHER = .02f;
@IntDef(
value = {
STATE_READING_SIGNAL_BYTE,
STATE_READING_INIT_VECTOR,
STATE_READING_SUBSAMPLE_ENCRYPTION_SIZE,
STATE_READING_SUBSAMPLE_ENCRYPTION_DATA
})
private @interface EncryptionDataReadState {}
private static final int STATE_READING_SIGNAL_BYTE = 0;
private static final int STATE_READING_INIT_VECTOR = 1;
private static final int STATE_READING_SUBSAMPLE_ENCRYPTION_SIZE = 2;
private static final int STATE_READING_SUBSAMPLE_ENCRYPTION_DATA = 3;
// Instance creation methods.
/**
* Creates an instance backed by the parser with the given {@code name}. The returned instance
* will attempt parsing without sniffing the content.
*
* @param name The name of the parser that will be associated with the created instance.
* @param outputConsumer The {@link OutputConsumer} to which track data and samples are pushed.
* @return A new instance.
* @throws IllegalArgumentException If an invalid name is provided.
*/
@NonNull
public static MediaParser createByName(
@NonNull @ParserName String name, @NonNull OutputConsumer outputConsumer) {
String[] nameAsArray = new String[] {name};
assertValidNames(nameAsArray);
return new MediaParser(outputConsumer, /* createdByName= */ true, name);
}
/**
* Creates an instance whose backing parser will be selected by sniffing the content during the
* first {@link #advance} call. Parser implementations will sniff the content in order of
* appearance in {@code parserNames}.
*
* @param outputConsumer The {@link OutputConsumer} to which extracted data is output.
* @param parserNames The names of the parsers to sniff the content with. If empty, a default
* array of names is used.
* @return A new instance.
*/
@NonNull
public static MediaParser create(
@NonNull OutputConsumer outputConsumer, @NonNull @ParserName String... parserNames) {
assertValidNames(parserNames);
if (parserNames.length == 0) {
parserNames = EXTRACTOR_FACTORIES_BY_NAME.keySet().toArray(new String[0]);
}
return new MediaParser(outputConsumer, /* createdByName= */ false, parserNames);
}
// Misc static methods.
/**
* Returns an immutable list with the names of the parsers that are suitable for container
* formats with the given {@link MediaFormat}.
*
* <p>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<String> getParserNames(@NonNull MediaFormat mediaFormat) {
String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
mimeType = mimeType == null ? null : Util.toLowerInvariant(mimeType.trim());
if (TextUtils.isEmpty(mimeType)) {
// No MIME type provided. Return all.
return Collections.unmodifiableList(
new ArrayList<>(EXTRACTOR_FACTORIES_BY_NAME.keySet()));
}
ArrayList<String> result = new ArrayList<>();
switch (mimeType) {
case "video/x-matroska":
case "audio/x-matroska":
case "video/x-webm":
case "audio/x-webm":
result.add(PARSER_NAME_MATROSKA);
break;
case "video/mp4":
case "audio/mp4":
case "application/mp4":
result.add(PARSER_NAME_MP4);
result.add(PARSER_NAME_FMP4);
break;
case "audio/mpeg":
result.add(PARSER_NAME_MP3);
break;
case "audio/aac":
result.add(PARSER_NAME_ADTS);
break;
case "audio/ac3":
result.add(PARSER_NAME_AC3);
break;
case "video/mp2t":
case "audio/mp2t":
result.add(PARSER_NAME_TS);
break;
case "video/x-flv":
result.add(PARSER_NAME_FLV);
break;
case "video/ogg":
case "audio/ogg":
case "application/ogg":
result.add(PARSER_NAME_OGG);
break;
case "video/mp2p":
case "video/mp1s":
result.add(PARSER_NAME_PS);
break;
case "audio/vnd.wave":
case "audio/wav":
case "audio/wave":
case "audio/x-wav":
result.add(PARSER_NAME_WAV);
break;
case "audio/amr":
result.add(PARSER_NAME_AMR);
break;
case "audio/ac4":
result.add(PARSER_NAME_AC4);
break;
case "audio/flac":
case "audio/x-flac":
result.add(PARSER_NAME_FLAC);
break;
default:
// No parsers support the given mime type. Do nothing.
break;
}
return Collections.unmodifiableList(result);
}
// Private fields.
private final Map<String, Object> mParserParameters;
private final OutputConsumer mOutputConsumer;
private final String[] mParserNamesPool;
private final PositionHolder mPositionHolder;
private final InputReadingDataReader mExoDataReader;
private final DataReaderAdapter mScratchDataReaderAdapter;
private final ParsableByteArrayAdapter mScratchParsableByteArrayAdapter;
@Nullable private final Constructor<DrmInitData.SchemeInitData> mSchemeInitDataConstructor;
private final ArrayList<Format> mMuxedCaptionFormats;
private boolean mInBandCryptoInfo;
private boolean mIncludeSupplementalData;
private boolean mIgnoreTimestampOffset;
private boolean mEagerlyExposeTrackType;
private boolean mExposeDummySeekMap;
private boolean mExposeChunkIndexAsMediaFormat;
private String mParserName;
private Extractor mExtractor;
private ExtractorInput mExtractorInput;
private boolean mPendingExtractorInit;
private long mPendingSeekPosition;
private long mPendingSeekTimeMicros;
private boolean mLoggedSchemeInitDataCreationException;
private boolean mReleased;
// MediaMetrics fields.
private final boolean mCreatedByName;
private final SparseArray<Format> mTrackFormats;
private String mLastObservedExceptionName;
private long mDurationMillis;
private long mResourceByteCount;
// Public methods.
/**
* Sets parser-specific parameters which allow customizing behavior.
*
* <p>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<MediaFormat>) value);
}
mParserParameters.put(parameterName, value);
return this;
}
/**
* Returns whether the given {@code parameterName} is supported by this parser.
*
* @param parameterName The parameter name to check support for. One of the {@code PARAMETER_*}
* constants.
* @return Whether the given {@code parameterName} is supported.
*/
public boolean supportsParameter(@NonNull @ParameterName String parameterName) {
return EXPECTED_TYPE_BY_PARAMETER_NAME.containsKey(parameterName);
}
/**
* Returns the name of the backing parser implementation.
*
* <p>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.
*
* <p>This method will block until some progress has been made.
*
* <p>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.
*
* <p>{@link SeekPoint SeekPoints} can be obtained from the {@link SeekMap} passed to {@link
* OutputConsumer#onSeekMapFound(SeekMap)}.
*
* <p>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.
*
* <p>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(
mParserName,
mCreatedByName,
String.join(MEDIAMETRICS_ELEMENT_SEPARATOR, mParserNamesPool),
mLastObservedExceptionName,
addDither(mResourceByteCount),
addDither(mDurationMillis),
trackMimeTypes,
trackCodecs,
alteredParameters,
videoWidth,
videoHeight);
}
// 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<Format, String> formatFieldGetter) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < mTrackFormats.size(); i++) {
if (i > 0) {
stringBuilder.append(MEDIAMETRICS_ELEMENT_SEPARATOR);
}
String fieldValue = formatFieldGetter.apply(mTrackFormats.valueAt(i));
stringBuilder.append(fieldValue != null ? fieldValue : "");
}
return stringBuilder.substring(
0, Math.min(stringBuilder.length(), MEDIAMETRICS_MAX_STRING_SIZE));
}
private void setMuxedCaptionFormats(List<MediaFormat> mediaFormats) {
mMuxedCaptionFormats.clear();
for (MediaFormat mediaFormat : mediaFormats) {
mMuxedCaptionFormats.add(toExoPlayerCaptionFormat(mediaFormat));
}
}
private boolean isPendingSeek() {
return mPendingSeekPosition >= 0;
}
private void removePendingSeek() {
mPendingSeekPosition = -1;
mPendingSeekTimeMicros = -1;
}
private Extractor createExtractor(String parserName) {
int flags = 0;
TimestampAdjuster timestampAdjuster = null;
if (mIgnoreTimestampOffset) {
timestampAdjuster = new TimestampAdjuster(TimestampAdjuster.DO_NOT_OFFSET);
}
switch (parserName) {
case PARSER_NAME_MATROSKA:
flags =
getBooleanParameter(PARAMETER_MATROSKA_DISABLE_CUES_SEEKING)
? MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES
: 0;
return new MatroskaExtractor(flags);
case PARSER_NAME_FMP4:
flags |=
getBooleanParameter(PARAMETER_EXPOSE_EMSG_TRACK)
? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK
: 0;
flags |=
getBooleanParameter(PARAMETER_MP4_IGNORE_EDIT_LISTS)
? FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_EDIT_LISTS
: 0;
flags |=
getBooleanParameter(PARAMETER_MP4_IGNORE_TFDT_BOX)
? FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX
: 0;
flags |=
getBooleanParameter(PARAMETER_MP4_TREAT_VIDEO_FRAMES_AS_KEYFRAMES)
? FragmentedMp4Extractor
.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME
: 0;
return new FragmentedMp4Extractor(
flags,
timestampAdjuster,
/* sideloadedTrack= */ null,
mMuxedCaptionFormats);
case PARSER_NAME_MP4:
flags |=
getBooleanParameter(PARAMETER_MP4_IGNORE_EDIT_LISTS)
? Mp4Extractor.FLAG_WORKAROUND_IGNORE_EDIT_LISTS
: 0;
return new Mp4Extractor(flags);
case PARSER_NAME_MP3:
flags |=
getBooleanParameter(PARAMETER_MP3_DISABLE_ID3)
? Mp3Extractor.FLAG_DISABLE_ID3_METADATA
: 0;
flags |=
getBooleanParameter(PARAMETER_MP3_ENABLE_CBR_SEEKING)
? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
: 0;
// TODO: Add index seeking once we update the ExoPlayer version.
return new Mp3Extractor(flags);
case PARSER_NAME_ADTS:
flags |=
getBooleanParameter(PARAMETER_ADTS_ENABLE_CBR_SEEKING)
? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
: 0;
return new AdtsExtractor(flags);
case PARSER_NAME_AC3:
return new Ac3Extractor();
case PARSER_NAME_TS:
flags |=
getBooleanParameter(PARAMETER_TS_ALLOW_NON_IDR_AVC_KEYFRAMES)
? DefaultTsPayloadReaderFactory.FLAG_ALLOW_NON_IDR_KEYFRAMES
: 0;
flags |=
getBooleanParameter(PARAMETER_TS_DETECT_ACCESS_UNITS)
? DefaultTsPayloadReaderFactory.FLAG_DETECT_ACCESS_UNITS
: 0;
flags |=
getBooleanParameter(PARAMETER_TS_ENABLE_HDMV_DTS_AUDIO_STREAMS)
? DefaultTsPayloadReaderFactory.FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS
: 0;
flags |=
getBooleanParameter(PARAMETER_TS_IGNORE_AAC_STREAM)
? DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM
: 0;
flags |=
getBooleanParameter(PARAMETER_TS_IGNORE_AVC_STREAM)
? DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM
: 0;
flags |=
getBooleanParameter(PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM)
? DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM
: 0;
flags |=
getBooleanParameter(PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS)
? DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS
: 0;
String tsMode = getStringParameter(PARAMETER_TS_MODE, TS_MODE_SINGLE_PMT);
int hlsMode =
TS_MODE_SINGLE_PMT.equals(tsMode)
? TsExtractor.MODE_SINGLE_PMT
: TS_MODE_HLS.equals(tsMode)
? TsExtractor.MODE_HLS
: TsExtractor.MODE_MULTI_PMT;
return new TsExtractor(
hlsMode,
timestampAdjuster != null
? timestampAdjuster
: new TimestampAdjuster(/* firstSampleTimestampUs= */ 0),
new DefaultTsPayloadReaderFactory(flags, mMuxedCaptionFormats));
case PARSER_NAME_FLV:
return new FlvExtractor();
case PARSER_NAME_OGG:
return new OggExtractor();
case PARSER_NAME_PS:
return new PsExtractor();
case PARSER_NAME_WAV:
return new WavExtractor();
case PARSER_NAME_AMR:
flags |=
getBooleanParameter(PARAMETER_AMR_ENABLE_CBR_SEEKING)
? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
: 0;
return new AmrExtractor(flags);
case PARSER_NAME_AC4:
return new Ac4Extractor();
case PARSER_NAME_FLAC:
flags |=
getBooleanParameter(PARAMETER_FLAC_DISABLE_ID3)
? FlacExtractor.FLAG_DISABLE_ID3_METADATA
: 0;
return new FlacExtractor(flags);
default:
// Should never happen.
throw new IllegalStateException("Unexpected attempt to create: " + parserName);
}
}
private boolean getBooleanParameter(String name) {
return (boolean) mParserParameters.getOrDefault(name, false);
}
private String getStringParameter(String name, String defaultValue) {
return (String) mParserParameters.getOrDefault(name, defaultValue);
}
// Private classes.
private static final class InputReadingDataReader implements DataReader {
public InputReader mInputReader;
@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
return mInputReader.read(buffer, offset, readLength);
}
}
private final class MediaParserDrmInitData extends DrmInitData {
private final SchemeInitData[] mSchemeDatas;
private MediaParserDrmInitData(com.google.android.exoplayer2.drm.DrmInitData exoDrmInitData)
throws IllegalAccessException, InstantiationException, InvocationTargetException {
mSchemeDatas = new SchemeInitData[exoDrmInitData.schemeDataCount];
for (int i = 0; i < mSchemeDatas.length; i++) {
mSchemeDatas[i] = toFrameworkSchemeInitData(exoDrmInitData.get(i));
}
}
@Override
@Nullable
public SchemeInitData get(UUID schemeUuid) {
for (SchemeInitData schemeInitData : mSchemeDatas) {
if (schemeInitData.uuid.equals(schemeUuid)) {
return schemeInitData;
}
}
return null;
}
@Override
public SchemeInitData getSchemeInitDataAt(int index) {
return mSchemeDatas[index];
}
@Override
public int getSchemeInitDataCount() {
return mSchemeDatas.length;
}
private DrmInitData.SchemeInitData toFrameworkSchemeInitData(SchemeData exoSchemeData)
throws IllegalAccessException, InvocationTargetException, InstantiationException {
return mSchemeInitDataConstructor.newInstance(
exoSchemeData.uuid, exoSchemeData.mimeType, exoSchemeData.data);
}
}
private final class ExtractorOutputAdapter implements ExtractorOutput {
private final SparseArray<TrackOutput> mTrackOutputAdapters;
private boolean mTracksEnded;
private ExtractorOutputAdapter() {
mTrackOutputAdapters = new SparseArray<>();
}
@Override
public TrackOutput track(int id, int type) {
TrackOutput trackOutput = mTrackOutputAdapters.get(id);
if (trackOutput == null) {
int trackIndex = mTrackOutputAdapters.size();
trackOutput = new TrackOutputAdapter(trackIndex);
mTrackOutputAdapters.put(id, trackOutput);
if (mEagerlyExposeTrackType) {
MediaFormat mediaFormat = new MediaFormat();
mediaFormat.setString("track-type-string", toTypeString(type));
mOutputConsumer.onTrackDataFound(
trackIndex, new TrackData(mediaFormat, /* drmInitData= */ null));
}
}
return trackOutput;
}
@Override
public void endTracks() {
mOutputConsumer.onTrackCountFound(mTrackOutputAdapters.size());
}
@Override
public void seekMap(com.google.android.exoplayer2.extractor.SeekMap exoplayerSeekMap) {
long durationUs = exoplayerSeekMap.getDurationUs();
if (durationUs != C.TIME_UNSET) {
mDurationMillis = C.usToMs(durationUs);
}
if (mExposeChunkIndexAsMediaFormat && exoplayerSeekMap instanceof ChunkIndex) {
ChunkIndex chunkIndex = (ChunkIndex) exoplayerSeekMap;
MediaFormat mediaFormat = new MediaFormat();
mediaFormat.setByteBuffer("chunk-index-int-sizes", toByteBuffer(chunkIndex.sizes));
mediaFormat.setByteBuffer(
"chunk-index-long-offsets", toByteBuffer(chunkIndex.offsets));
mediaFormat.setByteBuffer(
"chunk-index-long-us-durations", toByteBuffer(chunkIndex.durationsUs));
mediaFormat.setByteBuffer(
"chunk-index-long-us-times", toByteBuffer(chunkIndex.timesUs));
mOutputConsumer.onTrackDataFound(
/* trackIndex= */ 0, new TrackData(mediaFormat, /* drmInitData= */ null));
}
mOutputConsumer.onSeekMapFound(new SeekMap(exoplayerSeekMap));
}
}
private class TrackOutputAdapter implements TrackOutput {
private final int mTrackIndex;
private CryptoInfo mLastOutputCryptoInfo;
private CryptoInfo.Pattern mLastOutputEncryptionPattern;
private CryptoData mLastReceivedCryptoData;
@EncryptionDataReadState private int mEncryptionDataReadState;
private int mEncryptionDataSizeToSubtractFromSampleDataSize;
private int mEncryptionVectorSize;
private byte[] mScratchIvSpace;
private int mSubsampleEncryptionDataSize;
private int[] mScratchSubsampleEncryptedBytesCount;
private int[] mScratchSubsampleClearBytesCount;
private boolean mHasSubsampleEncryptionData;
private int mSkippedSupplementalDataBytes;
private TrackOutputAdapter(int trackIndex) {
mTrackIndex = trackIndex;
mScratchIvSpace = new byte[16]; // Size documented in CryptoInfo.
mScratchSubsampleEncryptedBytesCount = new int[32];
mScratchSubsampleClearBytesCount = new int[32];
mEncryptionDataReadState = STATE_READING_SIGNAL_BYTE;
mLastOutputEncryptionPattern =
new CryptoInfo.Pattern(/* blocksToEncrypt= */ 0, /* blocksToSkip= */ 0);
}
@Override
public void format(Format format) {
mTrackFormats.put(mTrackIndex, format);
mOutputConsumer.onTrackDataFound(
mTrackIndex,
new TrackData(
toMediaFormat(format), toFrameworkDrmInitData(format.drmInitData)));
}
@Override
public int sampleData(
DataReader input,
int length,
boolean allowEndOfInput,
@SampleDataPart int sampleDataPart)
throws IOException {
mScratchDataReaderAdapter.setDataReader(input, length);
long positionBeforeReading = mScratchDataReaderAdapter.getPosition();
mOutputConsumer.onSampleDataFound(mTrackIndex, mScratchDataReaderAdapter);
return (int) (mScratchDataReaderAdapter.getPosition() - positionBeforeReading);
}
@Override
public void sampleData(
ParsableByteArray data, int length, @SampleDataPart int sampleDataPart) {
if (sampleDataPart == SAMPLE_DATA_PART_ENCRYPTION && !mInBandCryptoInfo) {
while (length > 0) {
switch (mEncryptionDataReadState) {
case STATE_READING_SIGNAL_BYTE:
int encryptionSignalByte = data.readUnsignedByte();
length--;
mHasSubsampleEncryptionData = ((encryptionSignalByte >> 7) & 1) != 0;
mEncryptionVectorSize = encryptionSignalByte & 0x7F;
mEncryptionDataSizeToSubtractFromSampleDataSize =
mEncryptionVectorSize + 1; // Signal byte.
mEncryptionDataReadState = STATE_READING_INIT_VECTOR;
break;
case STATE_READING_INIT_VECTOR:
Arrays.fill(mScratchIvSpace, (byte) 0); // Ensure 0-padding.
data.readBytes(mScratchIvSpace, /* offset= */ 0, mEncryptionVectorSize);
length -= mEncryptionVectorSize;
if (mHasSubsampleEncryptionData) {
mEncryptionDataReadState = STATE_READING_SUBSAMPLE_ENCRYPTION_SIZE;
} else {
mSubsampleEncryptionDataSize = 0;
mEncryptionDataReadState = STATE_READING_SIGNAL_BYTE;
}
break;
case STATE_READING_SUBSAMPLE_ENCRYPTION_SIZE:
mSubsampleEncryptionDataSize = data.readUnsignedShort();
if (mScratchSubsampleClearBytesCount.length
< mSubsampleEncryptionDataSize) {
mScratchSubsampleClearBytesCount =
new int[mSubsampleEncryptionDataSize];
mScratchSubsampleEncryptedBytesCount =
new int[mSubsampleEncryptionDataSize];
}
length -= 2;
mEncryptionDataSizeToSubtractFromSampleDataSize +=
2
+ mSubsampleEncryptionDataSize
* BYTES_PER_SUBSAMPLE_ENCRYPTION_ENTRY;
mEncryptionDataReadState = STATE_READING_SUBSAMPLE_ENCRYPTION_DATA;
break;
case STATE_READING_SUBSAMPLE_ENCRYPTION_DATA:
for (int i = 0; i < mSubsampleEncryptionDataSize; i++) {
mScratchSubsampleClearBytesCount[i] = data.readUnsignedShort();
mScratchSubsampleEncryptedBytesCount[i] = data.readInt();
}
length -=
mSubsampleEncryptionDataSize
* BYTES_PER_SUBSAMPLE_ENCRYPTION_ENTRY;
mEncryptionDataReadState = STATE_READING_SIGNAL_BYTE;
if (length != 0) {
throw new IllegalStateException();
}
break;
default:
// Never happens.
throw new IllegalStateException();
}
}
} else if (sampleDataPart == SAMPLE_DATA_PART_SUPPLEMENTAL
&& !mIncludeSupplementalData) {
mSkippedSupplementalDataBytes += length;
data.skipBytes(length);
} else {
outputSampleData(data, length);
}
}
@Override
public void sampleMetadata(
long timeUs, int flags, int size, int offset, @Nullable CryptoData cryptoData) {
size -= mSkippedSupplementalDataBytes;
mSkippedSupplementalDataBytes = 0;
mOutputConsumer.onSampleCompleted(
mTrackIndex,
timeUs,
getMediaParserFlags(flags),
size - mEncryptionDataSizeToSubtractFromSampleDataSize,
offset,
getPopulatedCryptoInfo(cryptoData));
mEncryptionDataReadState = STATE_READING_SIGNAL_BYTE;
mEncryptionDataSizeToSubtractFromSampleDataSize = 0;
}
@Nullable
private CryptoInfo getPopulatedCryptoInfo(@Nullable CryptoData cryptoData) {
if (cryptoData == null) {
// The sample is not encrypted.
return null;
} else if (mInBandCryptoInfo) {
if (cryptoData != mLastReceivedCryptoData) {
mLastOutputCryptoInfo =
createNewCryptoInfoAndPopulateWithCryptoData(cryptoData);
// We are using in-band crypto info, so the IV will be ignored. But we prevent
// it from being null because toString assumes it non-null.
mLastOutputCryptoInfo.iv = EMPTY_BYTE_ARRAY;
}
} else /* We must populate the full CryptoInfo. */ {
// CryptoInfo.pattern is not accessible to the user, so the user needs to feed
// this CryptoInfo directly to MediaCodec. We need to create a new CryptoInfo per
// sample because of per-sample initialization vector changes.
CryptoInfo newCryptoInfo = createNewCryptoInfoAndPopulateWithCryptoData(cryptoData);
newCryptoInfo.iv = Arrays.copyOf(mScratchIvSpace, mScratchIvSpace.length);
boolean canReuseSubsampleInfo =
mLastOutputCryptoInfo != null
&& mLastOutputCryptoInfo.numSubSamples
== mSubsampleEncryptionDataSize;
for (int i = 0; i < mSubsampleEncryptionDataSize && canReuseSubsampleInfo; i++) {
canReuseSubsampleInfo =
mLastOutputCryptoInfo.numBytesOfClearData[i]
== mScratchSubsampleClearBytesCount[i]
&& mLastOutputCryptoInfo.numBytesOfEncryptedData[i]
== mScratchSubsampleEncryptedBytesCount[i];
}
newCryptoInfo.numSubSamples = mSubsampleEncryptionDataSize;
if (canReuseSubsampleInfo) {
newCryptoInfo.numBytesOfClearData = mLastOutputCryptoInfo.numBytesOfClearData;
newCryptoInfo.numBytesOfEncryptedData =
mLastOutputCryptoInfo.numBytesOfEncryptedData;
} else {
newCryptoInfo.numBytesOfClearData =
Arrays.copyOf(
mScratchSubsampleClearBytesCount, mSubsampleEncryptionDataSize);
newCryptoInfo.numBytesOfEncryptedData =
Arrays.copyOf(
mScratchSubsampleEncryptedBytesCount,
mSubsampleEncryptionDataSize);
}
mLastOutputCryptoInfo = newCryptoInfo;
}
mLastReceivedCryptoData = cryptoData;
return mLastOutputCryptoInfo;
}
private CryptoInfo createNewCryptoInfoAndPopulateWithCryptoData(CryptoData cryptoData) {
CryptoInfo cryptoInfo = new CryptoInfo();
cryptoInfo.key = cryptoData.encryptionKey;
cryptoInfo.mode = cryptoData.cryptoMode;
if (cryptoData.clearBlocks != mLastOutputEncryptionPattern.getSkipBlocks()
|| cryptoData.encryptedBlocks
!= mLastOutputEncryptionPattern.getEncryptBlocks()) {
mLastOutputEncryptionPattern =
new CryptoInfo.Pattern(cryptoData.encryptedBlocks, cryptoData.clearBlocks);
}
cryptoInfo.setPattern(mLastOutputEncryptionPattern);
return cryptoInfo;
}
private void outputSampleData(ParsableByteArray data, int length) {
mScratchParsableByteArrayAdapter.resetWithByteArray(data, length);
try {
// Read all bytes from data. ExoPlayer extractors expect all sample data to be
// consumed by TrackOutput implementations when passing a ParsableByteArray.
while (mScratchParsableByteArrayAdapter.getLength() > 0) {
mOutputConsumer.onSampleDataFound(
mTrackIndex, mScratchParsableByteArrayAdapter);
}
} catch (IOException e) {
// Unexpected.
throw new RuntimeException(e);
}
}
}
private static final class DataReaderAdapter implements InputReader {
private DataReader mDataReader;
private int mCurrentPosition;
private long mLength;
public void setDataReader(DataReader dataReader, long length) {
mDataReader = dataReader;
mCurrentPosition = 0;
mLength = length;
}
// Input implementation.
@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
int readBytes = 0;
readBytes = mDataReader.read(buffer, offset, readLength);
mCurrentPosition += readBytes;
return readBytes;
}
@Override
public long getPosition() {
return mCurrentPosition;
}
@Override
public long getLength() {
return mLength - mCurrentPosition;
}
}
private static final class ParsableByteArrayAdapter implements InputReader {
private ParsableByteArray mByteArray;
private long mLength;
private int mCurrentPosition;
public void resetWithByteArray(ParsableByteArray byteArray, long length) {
mByteArray = byteArray;
mCurrentPosition = 0;
mLength = length;
}
// Input implementation.
@Override
public int read(byte[] buffer, int offset, int readLength) {
mByteArray.readBytes(buffer, offset, readLength);
mCurrentPosition += readLength;
return readLength;
}
@Override
public long getPosition() {
return mCurrentPosition;
}
@Override
public long getLength() {
return mLength - mCurrentPosition;
}
}
private static final class DummyExoPlayerSeekMap
implements com.google.android.exoplayer2.extractor.SeekMap {
@Override
public boolean isSeekable() {
return true;
}
@Override
public long getDurationUs() {
return C.TIME_UNSET;
}
@Override
public SeekPoints getSeekPoints(long timeUs) {
com.google.android.exoplayer2.extractor.SeekPoint seekPoint =
new com.google.android.exoplayer2.extractor.SeekPoint(
timeUs, /* position= */ 0);
return new SeekPoints(seekPoint, seekPoint);
}
}
/** Creates extractor instances. */
private interface ExtractorFactory {
/** Returns a new extractor instance. */
Extractor createInstance();
}
// Private static methods.
private static Format toExoPlayerCaptionFormat(MediaFormat mediaFormat) {
Format.Builder formatBuilder =
new Format.Builder().setSampleMimeType(mediaFormat.getString(MediaFormat.KEY_MIME));
if (mediaFormat.containsKey(MediaFormat.KEY_CAPTION_SERVICE_NUMBER)) {
formatBuilder.setAccessibilityChannel(
mediaFormat.getInteger(MediaFormat.KEY_CAPTION_SERVICE_NUMBER));
}
return formatBuilder.build();
}
private static MediaFormat toMediaFormat(Format format) {
MediaFormat result = new MediaFormat();
setOptionalMediaFormatInt(result, MediaFormat.KEY_BIT_RATE, format.bitrate);
setOptionalMediaFormatInt(result, MediaFormat.KEY_CHANNEL_COUNT, format.channelCount);
ColorInfo colorInfo = format.colorInfo;
if (colorInfo != null) {
setOptionalMediaFormatInt(
result, MediaFormat.KEY_COLOR_TRANSFER, colorInfo.colorTransfer);
setOptionalMediaFormatInt(result, MediaFormat.KEY_COLOR_RANGE, colorInfo.colorRange);
setOptionalMediaFormatInt(result, MediaFormat.KEY_COLOR_STANDARD, colorInfo.colorSpace);
if (format.colorInfo.hdrStaticInfo != null) {
result.setByteBuffer(
MediaFormat.KEY_HDR_STATIC_INFO,
ByteBuffer.wrap(format.colorInfo.hdrStaticInfo));
}
}
setOptionalMediaFormatString(result, MediaFormat.KEY_MIME, format.sampleMimeType);
setOptionalMediaFormatString(result, MediaFormat.KEY_CODECS_STRING, format.codecs);
if (format.frameRate != Format.NO_VALUE) {
result.setFloat(MediaFormat.KEY_FRAME_RATE, format.frameRate);
}
setOptionalMediaFormatInt(result, MediaFormat.KEY_WIDTH, format.width);
setOptionalMediaFormatInt(result, MediaFormat.KEY_HEIGHT, format.height);
List<byte[]> initData = format.initializationData;
for (int i = 0; i < initData.size(); i++) {
result.setByteBuffer("csd-" + i, ByteBuffer.wrap(initData.get(i)));
}
setPcmEncoding(format, result);
setOptionalMediaFormatString(result, MediaFormat.KEY_LANGUAGE, format.language);
setOptionalMediaFormatInt(result, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize);
setOptionalMediaFormatInt(result, MediaFormat.KEY_ROTATION, format.rotationDegrees);
setOptionalMediaFormatInt(result, MediaFormat.KEY_SAMPLE_RATE, format.sampleRate);
setOptionalMediaFormatInt(
result, MediaFormat.KEY_CAPTION_SERVICE_NUMBER, format.accessibilityChannel);
int selectionFlags = format.selectionFlags;
result.setInteger(
MediaFormat.KEY_IS_AUTOSELECT, selectionFlags & C.SELECTION_FLAG_AUTOSELECT);
result.setInteger(MediaFormat.KEY_IS_DEFAULT, selectionFlags & C.SELECTION_FLAG_DEFAULT);
result.setInteger(
MediaFormat.KEY_IS_FORCED_SUBTITLE, selectionFlags & C.SELECTION_FLAG_FORCED);
setOptionalMediaFormatInt(result, MediaFormat.KEY_ENCODER_DELAY, format.encoderDelay);
setOptionalMediaFormatInt(result, MediaFormat.KEY_ENCODER_PADDING, format.encoderPadding);
if (format.pixelWidthHeightRatio != Format.NO_VALUE && format.pixelWidthHeightRatio != 0) {
int parWidth = 1;
int parHeight = 1;
if (format.pixelWidthHeightRatio < 1.0f) {
parHeight = 1 << 30;
parWidth = (int) (format.pixelWidthHeightRatio * parHeight);
} else if (format.pixelWidthHeightRatio > 1.0f) {
parWidth = 1 << 30;
parHeight = (int) (parWidth / format.pixelWidthHeightRatio);
}
result.setInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH, parWidth);
result.setInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT, parHeight);
result.setFloat("pixel-width-height-ratio-float", format.pixelWidthHeightRatio);
}
if (format.drmInitData != null) {
// The crypto mode is propagated along with sample metadata. We also include it in the
// format for convenient use from ExoPlayer.
result.setString("crypto-mode-fourcc", format.drmInitData.schemeType);
}
if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
result.setLong("subsample-offset-us-long", format.subsampleOffsetUs);
}
// LACK OF SUPPORT FOR:
// format.id;
// format.metadata;
// format.stereoMode;
return result;
}
private static ByteBuffer toByteBuffer(long[] longArray) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(longArray.length * Long.BYTES);
for (long element : longArray) {
byteBuffer.putLong(element);
}
byteBuffer.flip();
return byteBuffer;
}
private static ByteBuffer toByteBuffer(int[] intArray) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(intArray.length * Integer.BYTES);
for (int element : intArray) {
byteBuffer.putInt(element);
}
byteBuffer.flip();
return byteBuffer;
}
private static String toTypeString(int type) {
switch (type) {
case C.TRACK_TYPE_VIDEO:
return "video";
case C.TRACK_TYPE_AUDIO:
return "audio";
case C.TRACK_TYPE_TEXT:
return "text";
case C.TRACK_TYPE_METADATA:
return "metadata";
default:
return "unknown";
}
}
private static void setPcmEncoding(Format format, MediaFormat result) {
int exoPcmEncoding = format.pcmEncoding;
setOptionalMediaFormatInt(result, "exo-pcm-encoding", format.pcmEncoding);
int mediaFormatPcmEncoding;
switch (exoPcmEncoding) {
case C.ENCODING_PCM_8BIT:
mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_8BIT;
break;
case C.ENCODING_PCM_16BIT:
mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_16BIT;
break;
case C.ENCODING_PCM_FLOAT:
mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_FLOAT;
break;
default:
// No matching value. Do nothing.
return;
}
result.setInteger(MediaFormat.KEY_PCM_ENCODING, mediaFormatPcmEncoding);
}
private static void setOptionalMediaFormatInt(MediaFormat mediaFormat, String key, int value) {
if (value != Format.NO_VALUE) {
mediaFormat.setInteger(key, value);
}
}
private static void setOptionalMediaFormatString(
MediaFormat mediaFormat, String key, @Nullable String value) {
if (value != null) {
mediaFormat.setString(key, value);
}
}
private DrmInitData toFrameworkDrmInitData(
com.google.android.exoplayer2.drm.DrmInitData exoDrmInitData) {
try {
return exoDrmInitData != null && mSchemeInitDataConstructor != null
? new MediaParserDrmInitData(exoDrmInitData)
: null;
} catch (Throwable e) {
if (!mLoggedSchemeInitDataCreationException) {
mLoggedSchemeInitDataCreationException = true;
Log.e(TAG, "Unable to create SchemeInitData instance.");
}
return null;
}
}
/** Returns a new {@link SeekPoint} equivalent to the given {@code exoPlayerSeekPoint}. */
private static SeekPoint toSeekPoint(
com.google.android.exoplayer2.extractor.SeekPoint exoPlayerSeekPoint) {
return new SeekPoint(exoPlayerSeekPoint.timeUs, exoPlayerSeekPoint.position);
}
/**
* Introduces random error to the given metric value in order to prevent the identification of
* the parsed media.
*/
private static long addDither(long value) {
// Generate a random in [0, 1].
double randomDither = ThreadLocalRandom.current().nextFloat();
// Clamp the random number to [0, 2 * MEDIAMETRICS_DITHER].
randomDither *= 2 * MEDIAMETRICS_DITHER;
// Translate the random number to [1 - MEDIAMETRICS_DITHER, 1 + MEDIAMETRICS_DITHER].
randomDither += 1 - MEDIAMETRICS_DITHER;
return value != -1 ? (long) (value * randomDither) : -1;
}
private static void assertValidNames(@NonNull String[] names) {
for (String name : names) {
if (!EXTRACTOR_FACTORIES_BY_NAME.containsKey(name)) {
throw new IllegalArgumentException(
"Invalid extractor name: "
+ name
+ ". Supported parsers are: "
+ TextUtils.join(", ", EXTRACTOR_FACTORIES_BY_NAME.keySet())
+ ".");
}
}
}
private int getMediaParserFlags(int flags) {
@SampleFlags int result = 0;
result |= (flags & C.BUFFER_FLAG_ENCRYPTED) != 0 ? SAMPLE_FLAG_ENCRYPTED : 0;
result |= (flags & C.BUFFER_FLAG_KEY_FRAME) != 0 ? SAMPLE_FLAG_KEY_FRAME : 0;
result |= (flags & C.BUFFER_FLAG_DECODE_ONLY) != 0 ? SAMPLE_FLAG_DECODE_ONLY : 0;
result |=
(flags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0 && mIncludeSupplementalData
? SAMPLE_FLAG_HAS_SUPPLEMENTAL_DATA
: 0;
result |= (flags & C.BUFFER_FLAG_LAST_SAMPLE) != 0 ? SAMPLE_FLAG_LAST_SAMPLE : 0;
return result;
}
@Nullable
private static Constructor<DrmInitData.SchemeInitData> getSchemeInitDataConstructor() {
// TODO: Use constructor statically when available.
Constructor<DrmInitData.SchemeInitData> constructor;
try {
return DrmInitData.SchemeInitData.class.getConstructor(
UUID.class, String.class, byte[].class);
} catch (Throwable e) {
Log.e(TAG, "Unable to get SchemeInitData constructor.");
return null;
}
}
// Native methods.
private native void nativeSubmitMetrics(
String parserName,
boolean createdByName,
String parserPool,
String lastObservedExceptionName,
long resourceByteCount,
long durationMillis,
String trackMimeTypes,
String trackCodecs,
String alteredParameters,
int videoWidth,
int videoHeight);
// Static initialization.
static {
System.loadLibrary(JNI_LIBRARY_NAME);
// Using a LinkedHashMap to keep the insertion order when iterating over the keys.
LinkedHashMap<String, ExtractorFactory> extractorFactoriesByName = new LinkedHashMap<>();
// Parsers are ordered to match ExoPlayer's DefaultExtractorsFactory extractor ordering,
// which in turn aims to minimize the chances of incorrect extractor selections.
extractorFactoriesByName.put(PARSER_NAME_MATROSKA, MatroskaExtractor::new);
extractorFactoriesByName.put(PARSER_NAME_FMP4, FragmentedMp4Extractor::new);
extractorFactoriesByName.put(PARSER_NAME_MP4, Mp4Extractor::new);
extractorFactoriesByName.put(PARSER_NAME_MP3, Mp3Extractor::new);
extractorFactoriesByName.put(PARSER_NAME_ADTS, AdtsExtractor::new);
extractorFactoriesByName.put(PARSER_NAME_AC3, Ac3Extractor::new);
extractorFactoriesByName.put(PARSER_NAME_TS, TsExtractor::new);
extractorFactoriesByName.put(PARSER_NAME_FLV, FlvExtractor::new);
extractorFactoriesByName.put(PARSER_NAME_OGG, OggExtractor::new);
extractorFactoriesByName.put(PARSER_NAME_PS, PsExtractor::new);
extractorFactoriesByName.put(PARSER_NAME_WAV, WavExtractor::new);
extractorFactoriesByName.put(PARSER_NAME_AMR, AmrExtractor::new);
extractorFactoriesByName.put(PARSER_NAME_AC4, Ac4Extractor::new);
extractorFactoriesByName.put(PARSER_NAME_FLAC, FlacExtractor::new);
EXTRACTOR_FACTORIES_BY_NAME = Collections.unmodifiableMap(extractorFactoriesByName);
HashMap<String, Class> expectedTypeByParameterName = new HashMap<>();
expectedTypeByParameterName.put(PARAMETER_ADTS_ENABLE_CBR_SEEKING, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_AMR_ENABLE_CBR_SEEKING, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_FLAC_DISABLE_ID3, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_MP4_IGNORE_EDIT_LISTS, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_MP4_IGNORE_TFDT_BOX, Boolean.class);
expectedTypeByParameterName.put(
PARAMETER_MP4_TREAT_VIDEO_FRAMES_AS_KEYFRAMES, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_MATROSKA_DISABLE_CUES_SEEKING, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_MP3_DISABLE_ID3, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_MP3_ENABLE_CBR_SEEKING, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_MP3_ENABLE_INDEX_SEEKING, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_TS_MODE, String.class);
expectedTypeByParameterName.put(PARAMETER_TS_ALLOW_NON_IDR_AVC_KEYFRAMES, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_TS_IGNORE_AAC_STREAM, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_TS_IGNORE_AVC_STREAM, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_TS_DETECT_ACCESS_UNITS, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_TS_ENABLE_HDMV_DTS_AUDIO_STREAMS, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_IN_BAND_CRYPTO_INFO, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_INCLUDE_SUPPLEMENTAL_DATA, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_IGNORE_TIMESTAMP_OFFSET, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_EAGERLY_EXPOSE_TRACKTYPE, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_EXPOSE_DUMMY_SEEKMAP, Boolean.class);
expectedTypeByParameterName.put(
PARAMETER_EXPOSE_CHUNK_INDEX_AS_MEDIA_FORMAT, Boolean.class);
expectedTypeByParameterName.put(
PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS, Boolean.class);
expectedTypeByParameterName.put(PARAMETER_EXPOSE_EMSG_TRACK, Boolean.class);
// We do not check PARAMETER_EXPOSE_CAPTION_FORMATS here, and we do it in setParameters
// instead. Checking that the value is a List is insufficient to catch wrong parameter
// value types.
int sumOfParameterNameLengths =
expectedTypeByParameterName.keySet().stream()
.map(String::length)
.reduce(0, Integer::sum);
sumOfParameterNameLengths += PARAMETER_EXPOSE_CAPTION_FORMATS.length();
// Add space for any required separators.
MEDIAMETRICS_PARAMETER_LIST_MAX_LENGTH =
sumOfParameterNameLengths + expectedTypeByParameterName.size();
EXPECTED_TYPE_BY_PARAMETER_NAME = Collections.unmodifiableMap(expectedTypeByParameterName);
}
}