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