2294 lines
99 KiB
Java
2294 lines
99 KiB
Java
![]() |
/*
|
||
|
* Copyright (C) 2019 The Android Open Source Project
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
package android.media;
|
||
|
|
||
|
import android.annotation.CheckResult;
|
||
|
import android.annotation.IntDef;
|
||
|
import android.annotation.NonNull;
|
||
|
import android.annotation.Nullable;
|
||
|
import android.annotation.StringDef;
|
||
|
import android.media.MediaCodec.CryptoInfo;
|
||
|
import android.media.metrics.LogSessionId;
|
||
|
import android.os.Build;
|
||
|
import android.text.TextUtils;
|
||
|
import android.util.Log;
|
||
|
import android.util.Pair;
|
||
|
import android.util.SparseArray;
|
||
|
|
||
|
import androidx.annotation.RequiresApi;
|
||
|
|
||
|
import com.android.modules.utils.build.SdkLevel;
|
||
|
|
||
|
import com.google.android.exoplayer2.C;
|
||
|
import com.google.android.exoplayer2.Format;
|
||
|
import com.google.android.exoplayer2.ParserException;
|
||
|
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||
|
import com.google.android.exoplayer2.extractor.ChunkIndex;
|
||
|
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
|
||
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||
|
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||
|
import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints;
|
||
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||
|
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
|
||
|
import com.google.android.exoplayer2.extractor.flac.FlacExtractor;
|
||
|
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
|
||
|
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||
|
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
|
||
|
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
||
|
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
|
||
|
import com.google.android.exoplayer2.extractor.ogg.OggExtractor;
|
||
|
import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
|
||
|
import com.google.android.exoplayer2.extractor.ts.Ac4Extractor;
|
||
|
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
|
||
|
import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
|
||
|
import com.google.android.exoplayer2.extractor.ts.PsExtractor;
|
||
|
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
|
||
|
import com.google.android.exoplayer2.extractor.wav.WavExtractor;
|
||
|
import com.google.android.exoplayer2.upstream.DataReader;
|
||
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||
|
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||
|
import com.google.android.exoplayer2.video.ColorInfo;
|
||
|
import com.google.common.base.Ascii;
|
||
|
|
||
|
import java.io.EOFException;
|
||
|
import java.io.IOException;
|
||
|
import java.lang.annotation.Retention;
|
||
|
import java.lang.annotation.RetentionPolicy;
|
||
|
import java.lang.reflect.Constructor;
|
||
|
import java.lang.reflect.InvocationTargetException;
|
||
|
import java.nio.ByteBuffer;
|
||
|
import java.util.ArrayList;
|
||
|
import java.util.Arrays;
|
||
|
import java.util.Collections;
|
||
|
import java.util.HashMap;
|
||
|
import java.util.LinkedHashMap;
|
||
|
import java.util.List;
|
||
|
import java.util.Map;
|
||
|
import java.util.Objects;
|
||
|
import java.util.UUID;
|
||
|
import java.util.concurrent.ThreadLocalRandom;
|
||
|
import java.util.function.Function;
|
||
|
|
||
|
/**
|
||
|
* Parses media container formats and extracts contained media samples and metadata.
|
||
|
*
|
||
|
* <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>
|
||
|
*/
|
||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||
|
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 : Ascii.toLowerCase(mimeType);
|
||
|
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.
|
||
|
@Nullable private LogSessionId mLogSessionId;
|
||
|
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(
|
||
|
SdkLevel.isAtLeastS() ? getLogSessionIdStringV31() : "",
|
||
|
mParserName,
|
||
|
mCreatedByName,
|
||
|
String.join(MEDIAMETRICS_ELEMENT_SEPARATOR, mParserNamesPool),
|
||
|
mLastObservedExceptionName,
|
||
|
addDither(mResourceByteCount),
|
||
|
addDither(mDurationMillis),
|
||
|
trackMimeTypes,
|
||
|
trackCodecs,
|
||
|
alteredParameters,
|
||
|
videoWidth,
|
||
|
videoHeight);
|
||
|
}
|
||
|
|
||
|
@RequiresApi(31)
|
||
|
public void setLogSessionId(@NonNull LogSessionId logSessionId) {
|
||
|
this.mLogSessionId = Objects.requireNonNull(logSessionId);
|
||
|
}
|
||
|
|
||
|
@RequiresApi(31)
|
||
|
@NonNull
|
||
|
public LogSessionId getLogSessionId() {
|
||
|
return mLogSessionId != null ? mLogSessionId : LogSessionId.LOG_SESSION_ID_NONE;
|
||
|
}
|
||
|
|
||
|
// Private methods.
|
||
|
|
||
|
private MediaParser(
|
||
|
OutputConsumer outputConsumer, boolean createdByName, String... parserNamesPool) {
|
||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||
|
throw new UnsupportedOperationException("Android version must be R or greater.");
|
||
|
}
|
||
|
mParserParameters = new HashMap<>();
|
||
|
mOutputConsumer = outputConsumer;
|
||
|
mParserNamesPool = parserNamesPool;
|
||
|
mCreatedByName = createdByName;
|
||
|
mParserName = createdByName ? parserNamesPool[0] : PARSER_NAME_UNKNOWN;
|
||
|
mPositionHolder = new PositionHolder();
|
||
|
mExoDataReader = new InputReadingDataReader();
|
||
|
removePendingSeek();
|
||
|
mScratchDataReaderAdapter = new DataReaderAdapter();
|
||
|
mScratchParsableByteArrayAdapter = new ParsableByteArrayAdapter();
|
||
|
mSchemeInitDataConstructor = getSchemeInitDataConstructor();
|
||
|
mMuxedCaptionFormats = new ArrayList<>();
|
||
|
|
||
|
// MediaMetrics.
|
||
|
mTrackFormats = new SparseArray<>();
|
||
|
mLastObservedExceptionName = "";
|
||
|
mDurationMillis = -1;
|
||
|
}
|
||
|
|
||
|
private String buildMediaMetricsString(Function<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.MODE_NO_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);
|
||
|
}
|
||
|
|
||
|
@RequiresApi(31)
|
||
|
private String getLogSessionIdStringV31() {
|
||
|
return mLogSessionId != null ? mLogSessionId.getStringId() : "";
|
||
|
}
|
||
|
|
||
|
// 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 logSessionId,
|
||
|
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);
|
||
|
}
|
||
|
}
|