Publish components that depend on MediaParser

This change will be followed up by:
- Changes adding APIs to enable the use of MediaParser in each of the supported
  media sources.
- Changes removing TODOs related to the change of the stable SDK to API 30.

PiperOrigin-RevId: 339556777
This commit is contained in:
aquilescanta 2020-10-28 23:02:54 +00:00 committed by Oliver Woodman
parent d5170688b4
commit 04c56c44cf
10 changed files with 1429 additions and 6 deletions

View File

@ -0,0 +1,121 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source;
import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.PARAMETER_EAGERLY_EXPOSE_TRACK_TYPE;
import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.PARAMETER_INCLUDE_SUPPLEMENTAL_DATA;
import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.PARAMETER_IN_BAND_CRYPTO_INFO;
import android.annotation.SuppressLint;
import android.media.MediaParser;
import android.media.MediaParser.SeekPoint;
import android.net.Uri;
import android.util.Pair;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.source.mediaparser.InputReaderAdapterV30;
import com.google.android.exoplayer2.source.mediaparser.OutputConsumerAdapterV30;
import com.google.android.exoplayer2.upstream.DataReader;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/** {@link ProgressiveMediaExtractor} implemented on top of the platform's {@link MediaParser}. */
@RequiresApi(30)
/* package */ final class MediaParserExtractorAdapter implements ProgressiveMediaExtractor {
private final OutputConsumerAdapterV30 outputConsumerAdapter;
private final InputReaderAdapterV30 inputReaderAdapter;
private final MediaParser mediaParser;
private String parserName;
@SuppressLint("WrongConstant")
public MediaParserExtractorAdapter() {
// TODO: Add support for injecting the desired extractor list.
outputConsumerAdapter = new OutputConsumerAdapterV30();
inputReaderAdapter = new InputReaderAdapterV30();
mediaParser = MediaParser.create(outputConsumerAdapter);
mediaParser.setParameter(PARAMETER_EAGERLY_EXPOSE_TRACK_TYPE, true);
mediaParser.setParameter(PARAMETER_IN_BAND_CRYPTO_INFO, true);
mediaParser.setParameter(PARAMETER_INCLUDE_SUPPLEMENTAL_DATA, true);
parserName = MediaParser.PARSER_NAME_UNKNOWN;
}
@Override
public void init(
DataReader dataReader,
Uri uri,
Map<String, List<String>> responseHeaders,
long position,
long length,
ExtractorOutput output)
throws IOException {
outputConsumerAdapter.setExtractorOutput(output);
inputReaderAdapter.setDataReader(dataReader, length);
inputReaderAdapter.setCurrentPosition(position);
String currentParserName = mediaParser.getParserName();
if (MediaParser.PARSER_NAME_UNKNOWN.equals(currentParserName)) {
// We need to sniff.
mediaParser.advance(inputReaderAdapter);
parserName = mediaParser.getParserName();
outputConsumerAdapter.setSelectedParserName(parserName);
} else if (!currentParserName.equals(parserName)) {
// The parser was created by name.
parserName = mediaParser.getParserName();
outputConsumerAdapter.setSelectedParserName(parserName);
} else {
// The parser implementation has already been selected. Do nothing.
}
}
@Override
public void release() {
mediaParser.release();
}
@Override
public void disableSeekingOnMp3Streams() {
if (MediaParser.PARSER_NAME_MP3.equals(parserName)) {
outputConsumerAdapter.disableSeeking();
}
}
@Override
public long getCurrentInputPosition() {
return inputReaderAdapter.getPosition();
}
@Override
public void seek(long position, long seekTimeUs) {
inputReaderAdapter.setCurrentPosition(position);
Pair<SeekPoint, SeekPoint> seekPoints = outputConsumerAdapter.getSeekPoints(seekTimeUs);
mediaParser.seek(seekPoints.second.position == position ? seekPoints.second : seekPoints.first);
}
@Override
public int read(PositionHolder positionHolder) throws IOException {
boolean shouldContinue = mediaParser.advance(inputReaderAdapter);
positionHolder.position = inputReaderAdapter.getAndResetSeekPosition();
return !shouldContinue
? Extractor.RESULT_END_OF_INPUT
: positionHolder.position != C.POSITION_UNSET
? Extractor.RESULT_SEEK
: Extractor.RESULT_CONTINUE;
}
}

View File

@ -188,9 +188,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.customCacheKey = customCacheKey;
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
loader = new Loader("Loader:ProgressiveMediaPeriod");
ProgressiveMediaExtractor progressiveMediaExtractor =
new BundledExtractorsAdapter(extractorsFactory);
this.progressiveMediaExtractor = progressiveMediaExtractor;
this.progressiveMediaExtractor = new BundledExtractorsAdapter(extractorsFactory);
loadCondition = new ConditionVariable();
maybeFinishPrepareRunnable = this::maybeFinishPrepare;
onContinueLoadingRequestedRunnable =

View File

@ -0,0 +1,169 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.chunk;
import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.PARAMETER_EAGERLY_EXPOSE_TRACK_TYPE;
import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.PARAMETER_EXPOSE_CAPTION_FORMATS;
import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.PARAMETER_EXPOSE_CHUNK_INDEX_AS_MEDIA_FORMAT;
import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.PARAMETER_EXPOSE_DUMMY_SEEK_MAP;
import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.PARAMETER_INCLUDE_SUPPLEMENTAL_DATA;
import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.PARAMETER_IN_BAND_CRYPTO_INFO;
import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS;
import android.annotation.SuppressLint;
import android.media.MediaFormat;
import android.media.MediaParser;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.source.mediaparser.InputReaderAdapterV30;
import com.google.android.exoplayer2.source.mediaparser.MediaParserUtil;
import com.google.android.exoplayer2.source.mediaparser.OutputConsumerAdapterV30;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/** {@link ChunkExtractor} implemented on top of the platform's {@link MediaParser}. */
@RequiresApi(30)
public final class MediaParserChunkExtractor implements ChunkExtractor {
private final OutputConsumerAdapterV30 outputConsumerAdapter;
private final InputReaderAdapterV30 inputReaderAdapter;
private final MediaParser mediaParser;
private final TrackOutputProviderAdapter trackOutputProviderAdapter;
private final DummyTrackOutput dummyTrackOutput;
private long pendingSeekUs;
@Nullable private TrackOutputProvider trackOutputProvider;
@Nullable private Format[] sampleFormats;
/**
* Creates a new instance.
*
* @param primaryTrackType The type of the primary track, or {@link C#TRACK_TYPE_NONE} if there is
* no primary track. Must be one of the {@link C C.TRACK_TYPE_*} constants.
* @param manifestFormat The chunks {@link Format} as obtained from the manifest.
* @param closedCaptionFormats A list containing the {@link Format Formats} of the closed-caption
* tracks in the chunks.
*/
@SuppressLint("WrongConstant")
public MediaParserChunkExtractor(
int primaryTrackType, Format manifestFormat, List<Format> closedCaptionFormats) {
outputConsumerAdapter =
new OutputConsumerAdapterV30(
manifestFormat, primaryTrackType, /* expectDummySeekMap= */ true);
inputReaderAdapter = new InputReaderAdapterV30();
String mimeType = Assertions.checkNotNull(manifestFormat.containerMimeType);
String parserName =
MimeTypes.isMatroska(mimeType)
? MediaParser.PARSER_NAME_MATROSKA
: MediaParser.PARSER_NAME_FMP4;
outputConsumerAdapter.setSelectedParserName(parserName);
mediaParser = MediaParser.createByName(parserName, outputConsumerAdapter);
mediaParser.setParameter(MediaParser.PARAMETER_MATROSKA_DISABLE_CUES_SEEKING, true);
mediaParser.setParameter(PARAMETER_IN_BAND_CRYPTO_INFO, true);
mediaParser.setParameter(PARAMETER_INCLUDE_SUPPLEMENTAL_DATA, true);
mediaParser.setParameter(PARAMETER_EAGERLY_EXPOSE_TRACK_TYPE, true);
mediaParser.setParameter(PARAMETER_EXPOSE_DUMMY_SEEK_MAP, true);
mediaParser.setParameter(PARAMETER_EXPOSE_CHUNK_INDEX_AS_MEDIA_FORMAT, true);
mediaParser.setParameter(PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS, true);
ArrayList<MediaFormat> closedCaptionMediaFormats = new ArrayList<>();
for (int i = 0; i < closedCaptionFormats.size(); i++) {
closedCaptionMediaFormats.add(
MediaParserUtil.toCaptionsMediaFormat(closedCaptionFormats.get(i)));
}
mediaParser.setParameter(PARAMETER_EXPOSE_CAPTION_FORMATS, closedCaptionMediaFormats);
outputConsumerAdapter.setMuxedCaptionFormats(closedCaptionFormats);
trackOutputProviderAdapter = new TrackOutputProviderAdapter();
dummyTrackOutput = new DummyTrackOutput();
pendingSeekUs = C.TIME_UNSET;
}
// ChunkExtractor implementation.
@Override
public void init(
@Nullable TrackOutputProvider trackOutputProvider, long startTimeUs, long endTimeUs) {
this.trackOutputProvider = trackOutputProvider;
outputConsumerAdapter.setSampleTimestampUpperLimitFilterUs(endTimeUs);
outputConsumerAdapter.setExtractorOutput(trackOutputProviderAdapter);
pendingSeekUs = startTimeUs;
}
@Override
public void release() {
mediaParser.release();
}
@Override
public boolean read(ExtractorInput extractorInput) throws IOException {
maybeExecutePendingSeek();
inputReaderAdapter.setDataReader(extractorInput, extractorInput.getLength());
return mediaParser.advance(inputReaderAdapter);
}
@Nullable
@Override
public ChunkIndex getChunkIndex() {
return outputConsumerAdapter.getChunkIndex();
}
@Nullable
@Override
public Format[] getSampleFormats() {
return sampleFormats;
}
// Internal methods.
private void maybeExecutePendingSeek() {
@Nullable MediaParser.SeekMap dummySeekMap = outputConsumerAdapter.getDummySeekMap();
if (pendingSeekUs != C.TIME_UNSET && dummySeekMap != null) {
mediaParser.seek(dummySeekMap.getSeekPoints(pendingSeekUs).first);
pendingSeekUs = C.TIME_UNSET;
}
}
// Internal classes.
private class TrackOutputProviderAdapter implements ExtractorOutput {
@Override
public TrackOutput track(int id, int type) {
return trackOutputProvider != null ? trackOutputProvider.track(id, type) : dummyTrackOutput;
}
@Override
public void endTracks() {
// Imitate BundledChunkExtractor behavior, which captures a sample format snapshot when
// endTracks is called.
sampleFormats = outputConsumerAdapter.getSampleFormats();
}
@Override
public void seekMap(SeekMap seekMap) {
// Do nothing.
}
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.mediaparser;
import android.annotation.SuppressLint;
import android.media.MediaParser;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataReader;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/** {@link MediaParser.SeekableInputReader} implementation wrapping a {@link DataReader}. */
@RequiresApi(30)
@SuppressLint("Override") // TODO: Remove once the SDK becomes stable.
public final class InputReaderAdapterV30 implements MediaParser.SeekableInputReader {
@Nullable private DataReader dataReader;
private long resourceLength;
private long currentPosition;
private long lastSeekPosition;
/**
* Sets the wrapped {@link DataReader}.
*
* @param dataReader The {@link DataReader} to wrap.
* @param length The length of the resource from which {@code dataReader} reads.
*/
public void setDataReader(DataReader dataReader, long length) {
this.dataReader = dataReader;
resourceLength = length;
lastSeekPosition = C.POSITION_UNSET;
}
/** Sets the absolute position in the resource from which the wrapped {@link DataReader} reads. */
public void setCurrentPosition(long position) {
currentPosition = position;
}
/**
* Returns the last value passed to {@link #seekToPosition(long)} and sets the stored value to
* {@link C#POSITION_UNSET}.
*/
public long getAndResetSeekPosition() {
long lastSeekPosition = this.lastSeekPosition;
this.lastSeekPosition = C.POSITION_UNSET;
return lastSeekPosition;
}
// SeekableInputReader implementation.
@Override
public void seekToPosition(long position) {
lastSeekPosition = position;
}
@Override
public int read(byte[] bytes, int offset, int readLength) throws IOException {
int bytesRead = Util.castNonNull(dataReader).read(bytes, offset, readLength);
currentPosition += bytesRead;
return bytesRead;
}
@Override
public long getPosition() {
return currentPosition;
}
@Override
public long getLength() {
return resourceLength;
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.mediaparser;
import android.media.MediaFormat;
import android.media.MediaParser;
import com.google.android.exoplayer2.Format;
/**
* Miscellaneous constants and utility methods related to the {@link MediaParser} integration.
*
* <p>For documentation on constants, please see the {@link MediaParser} documentation.
*/
public final class MediaParserUtil {
public static final String PARAMETER_IN_BAND_CRYPTO_INFO =
"android.media.mediaparser.inBandCryptoInfo";
public static final String PARAMETER_INCLUDE_SUPPLEMENTAL_DATA =
"android.media.mediaparser.includeSupplementalData";
public static final String PARAMETER_EAGERLY_EXPOSE_TRACK_TYPE =
"android.media.mediaparser.eagerlyExposeTrackType";
public static final String PARAMETER_EXPOSE_DUMMY_SEEK_MAP =
"android.media.mediaparser.exposeDummySeekMap";
public static final String PARAMETER_EXPOSE_CHUNK_INDEX_AS_MEDIA_FORMAT =
"android.media.mediaParser.exposeChunkIndexAsMediaFormat";
public static final String PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS =
"android.media.mediaParser.overrideInBandCaptionDeclarations";
public static final String PARAMETER_EXPOSE_CAPTION_FORMATS =
"android.media.mediaParser.exposeCaptionFormats";
public static final String PARAMETER_IGNORE_TIMESTAMP_OFFSET =
"android.media.mediaparser.ignoreTimestampOffset";
private MediaParserUtil() {}
/**
* Returns a {@link MediaFormat} with equivalent {@link MediaFormat#KEY_MIME} and {@link
* MediaFormat#KEY_CAPTION_SERVICE_NUMBER} to the given {@link Format}.
*/
public static MediaFormat toCaptionsMediaFormat(Format format) {
MediaFormat mediaFormat = new MediaFormat();
mediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType);
if (format.accessibilityChannel != Format.NO_VALUE) {
mediaFormat.setInteger(MediaFormat.KEY_CAPTION_SERVICE_NUMBER, format.accessibilityChannel);
}
return mediaFormat;
}
}

View File

@ -0,0 +1,691 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.mediaparser;
import static android.media.MediaParser.PARSER_NAME_AC3;
import static android.media.MediaParser.PARSER_NAME_AC4;
import static android.media.MediaParser.PARSER_NAME_ADTS;
import static android.media.MediaParser.PARSER_NAME_AMR;
import static android.media.MediaParser.PARSER_NAME_FLAC;
import static android.media.MediaParser.PARSER_NAME_FLV;
import static android.media.MediaParser.PARSER_NAME_FMP4;
import static android.media.MediaParser.PARSER_NAME_MATROSKA;
import static android.media.MediaParser.PARSER_NAME_MP3;
import static android.media.MediaParser.PARSER_NAME_MP4;
import static android.media.MediaParser.PARSER_NAME_OGG;
import static android.media.MediaParser.PARSER_NAME_PS;
import static android.media.MediaParser.PARSER_NAME_TS;
import static android.media.MediaParser.PARSER_NAME_WAV;
import android.annotation.SuppressLint;
import android.media.DrmInitData.SchemeInitData;
import android.media.MediaCodec;
import android.media.MediaCodec.CryptoInfo;
import android.media.MediaFormat;
import android.media.MediaParser;
import android.media.MediaParser.TrackData;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.SelectionFlags;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.extractor.DummyExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData;
import com.google.android.exoplayer2.upstream.DataReader;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.ColorInfo;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.LongBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
* {@link MediaParser.OutputConsumer} implementation that redirects output to an {@link
* ExtractorOutput}.
*/
@RequiresApi(30)
@SuppressLint("Override") // TODO: Remove once the SDK becomes stable.
public final class OutputConsumerAdapterV30 implements MediaParser.OutputConsumer {
private static final String TAG = "OutputConsumerAdapterV30";
private static final Pair<MediaParser.SeekPoint, MediaParser.SeekPoint> SEEK_POINT_PAIR_START =
Pair.create(MediaParser.SeekPoint.START, MediaParser.SeekPoint.START);
private static final String MEDIA_FORMAT_KEY_TRACK_TYPE = "track-type-string";
private static final String MEDIA_FORMAT_KEY_CHUNK_INDEX_SIZES = "chunk-index-int-sizes";
private static final String MEDIA_FORMAT_KEY_CHUNK_INDEX_OFFSETS = "chunk-index-long-offsets";
private static final String MEDIA_FORMAT_KEY_CHUNK_INDEX_DURATIONS =
"chunk-index-long-us-durations";
private static final String MEDIA_FORMAT_KEY_CHUNK_INDEX_TIMES = "chunk-index-long-us-times";
private static final Pattern REGEX_CRYPTO_INFO_PATTERN =
Pattern.compile("pattern \\(encrypt: (\\d+), skip: (\\d+)\\)");
private final ArrayList<@NullableType TrackOutput> trackOutputs;
private final ArrayList<@NullableType Format> trackFormats;
private final ArrayList<@NullableType CryptoInfo> lastReceivedCryptoInfos;
private final ArrayList<@NullableType CryptoData> lastOutputCryptoDatas;
private final DataReaderAdapter scratchDataReaderAdapter;
private final boolean expectDummySeekMap;
private final int primaryTrackType;
@Nullable private final Format primaryTrackManifestFormat;
private ExtractorOutput extractorOutput;
@Nullable private MediaParser.SeekMap dummySeekMap;
@Nullable private MediaParser.SeekMap lastSeekMap;
@Nullable private String containerMimeType;
@Nullable private ChunkIndex lastChunkIndex;
@Nullable private TimestampAdjuster timestampAdjuster;
private List<Format> muxedCaptionFormats;
private int primaryTrackIndex;
private long sampleTimestampUpperLimitFilterUs;
private boolean tracksFoundCalled;
private boolean tracksEnded;
private boolean seekingDisabled;
/**
* Equivalent to {@link #OutputConsumerAdapterV30(Format, int, boolean)
* OutputConsumerAdapterV30(primaryTrackManifestFormat= null, primaryTrackType= C.TRACK_TYPE_NONE,
* expectDummySeekMap= false)}
*/
public OutputConsumerAdapterV30() {
this(
/* primaryTrackManifestFormat= */ null,
/* primaryTrackType= */ C.TRACK_TYPE_NONE,
/* expectDummySeekMap= */ false);
}
/**
* Creates a new instance.
*
* @param primaryTrackManifestFormat The manifest-obtained format of the primary track, or null if
* not applicable.
* @param primaryTrackType The type of the primary track, or {@link C#TRACK_TYPE_NONE} if there is
* no primary track. Must be one of the {@link C C.TRACK_TYPE_*} constants.
* @param expectDummySeekMap Whether the output consumer should expect an initial dummy seek map
* which should be exposed through {@link #getDummySeekMap()}.
*/
public OutputConsumerAdapterV30(
@Nullable Format primaryTrackManifestFormat,
int primaryTrackType,
boolean expectDummySeekMap) {
this.expectDummySeekMap = expectDummySeekMap;
this.primaryTrackManifestFormat = primaryTrackManifestFormat;
this.primaryTrackType = primaryTrackType;
trackOutputs = new ArrayList<>();
trackFormats = new ArrayList<>();
lastReceivedCryptoInfos = new ArrayList<>();
lastOutputCryptoDatas = new ArrayList<>();
scratchDataReaderAdapter = new DataReaderAdapter();
extractorOutput = new DummyExtractorOutput();
sampleTimestampUpperLimitFilterUs = C.TIME_UNSET;
muxedCaptionFormats = ImmutableList.of();
}
/**
* Sets an upper limit for sample timestamp filtering.
*
* <p>When set, samples with timestamps greater than {@code sampleTimestampUpperLimitFilterUs}
* will be discarded.
*
* @param sampleTimestampUpperLimitFilterUs The maximum allowed sample timestamp, or {@link
* C#TIME_UNSET} to remove filtering.
*/
public void setSampleTimestampUpperLimitFilterUs(long sampleTimestampUpperLimitFilterUs) {
this.sampleTimestampUpperLimitFilterUs = sampleTimestampUpperLimitFilterUs;
}
/** Sets a {@link TimestampAdjuster} for adjusting the timestamps of the output samples. */
public void setTimestampAdjuster(TimestampAdjuster timestampAdjuster) {
this.timestampAdjuster = timestampAdjuster;
}
/**
* Sets the {@link ExtractorOutput} to which {@link MediaParser MediaParser's} output is directed.
*/
public void setExtractorOutput(ExtractorOutput extractorOutput) {
this.extractorOutput = extractorOutput;
}
/** Sets {@link Format} information associated to the caption tracks multiplexed in the media. */
public void setMuxedCaptionFormats(List<Format> muxedCaptionFormats) {
this.muxedCaptionFormats = muxedCaptionFormats;
}
/** Overrides future received {@link SeekMap SeekMaps} with non-seekable instances. */
public void disableSeeking() {
seekingDisabled = true;
}
/**
* Returns a dummy {@link MediaParser.SeekMap}, or null if not available.
*
* <p>the dummy {@link MediaParser.SeekMap} returns a single {@link MediaParser.SeekPoint} whose
* {@link MediaParser.SeekPoint#timeMicros} matches the requested timestamp, and {@link
* MediaParser.SeekPoint#position} is 0.
*/
@Nullable
public MediaParser.SeekMap getDummySeekMap() {
return dummySeekMap;
}
/** Returns the most recently output {@link ChunkIndex}, or null if none has been output. */
@Nullable
public ChunkIndex getChunkIndex() {
return lastChunkIndex;
}
/**
* Returns the {@link MediaParser.SeekPoint} instances corresponding to the given timestamp.
*
* @param seekTimeUs The timestamp in microseconds to retrieve {@link MediaParser.SeekPoint}
* instances for.
* @return The {@link MediaParser.SeekPoint} instances corresponding to the given timestamp.
*/
public Pair<MediaParser.SeekPoint, MediaParser.SeekPoint> getSeekPoints(long seekTimeUs) {
return lastSeekMap != null ? lastSeekMap.getSeekPoints(seekTimeUs) : SEEK_POINT_PAIR_START;
}
/**
* Defines the container mime type to propagate through {@link TrackOutput#format}.
*
* @param parserName The name of the selected parser.
*/
public void setSelectedParserName(String parserName) {
containerMimeType = getMimeType(parserName);
}
/**
* Returns the last output format for each track, or null if not all the tracks have been
* identified.
*/
@Nullable
public Format[] getSampleFormats() {
if (!tracksFoundCalled) {
return null;
}
Format[] sampleFormats = new Format[trackFormats.size()];
for (int i = 0; i < trackFormats.size(); i++) {
sampleFormats[i] = Assertions.checkNotNull(trackFormats.get(i));
}
return sampleFormats;
}
// MediaParser.OutputConsumer implementation.
@Override
public void onTrackCountFound(int numberOfTracks) {
tracksFoundCalled = true;
maybeEndTracks();
}
@Override
public void onSeekMapFound(MediaParser.SeekMap seekMap) {
if (expectDummySeekMap && dummySeekMap == null) {
// This is a dummy seek map.
dummySeekMap = seekMap;
} else {
lastSeekMap = seekMap;
long durationUs = seekMap.getDurationMicros();
extractorOutput.seekMap(
seekingDisabled
? new SeekMap.Unseekable(
durationUs != MediaParser.SeekMap.UNKNOWN_DURATION ? durationUs : C.TIME_UNSET)
: new SeekMapAdapter(seekMap));
}
}
@Override
public void onTrackDataFound(int trackIndex, TrackData trackData) {
if (maybeObtainChunkIndex(trackData.mediaFormat)) {
// The MediaFormat contains a chunk index. It does not contain anything else.
return;
}
ensureSpaceForTrackIndex(trackIndex);
@Nullable TrackOutput trackOutput = trackOutputs.get(trackIndex);
if (trackOutput == null) {
@Nullable
String trackTypeString = trackData.mediaFormat.getString(MEDIA_FORMAT_KEY_TRACK_TYPE);
int trackType =
toTrackTypeConstant(
trackTypeString != null
? trackTypeString
: trackData.mediaFormat.getString(MediaFormat.KEY_MIME));
if (trackType == primaryTrackType) {
primaryTrackIndex = trackIndex;
}
trackOutput = extractorOutput.track(trackIndex, trackType);
trackOutputs.set(trackIndex, trackOutput);
if (trackTypeString != null) {
// The MediaFormat includes the track type string, so it cannot include any other keys, as
// per the android.media.mediaparser.eagerlyExposeTrackType parameter documentation.
return;
}
}
Format format = toExoPlayerFormat(trackData);
trackOutput.format(
primaryTrackManifestFormat != null && trackIndex == primaryTrackIndex
? format.withManifestFormatInfo(primaryTrackManifestFormat)
: format);
trackFormats.set(trackIndex, format);
maybeEndTracks();
}
@Override
public void onSampleDataFound(int trackIndex, MediaParser.InputReader sampleData)
throws IOException {
ensureSpaceForTrackIndex(trackIndex);
scratchDataReaderAdapter.input = sampleData;
TrackOutput trackOutput = trackOutputs.get(trackIndex);
if (trackOutput == null) {
trackOutput = extractorOutput.track(trackIndex, C.TRACK_TYPE_UNKNOWN);
trackOutputs.set(trackIndex, trackOutput);
}
trackOutput.sampleData(
scratchDataReaderAdapter, (int) sampleData.getLength(), /* allowEndOfInput= */ true);
}
@Override
public void onSampleCompleted(
int trackIndex,
long timeUs,
int flags,
int size,
int offset,
@Nullable MediaCodec.CryptoInfo cryptoInfo) {
if (sampleTimestampUpperLimitFilterUs != C.TIME_UNSET
&& timeUs >= sampleTimestampUpperLimitFilterUs) {
// Ignore this sample.
return;
} else if (timestampAdjuster != null) {
timeUs = timestampAdjuster.adjustSampleTimestamp(timeUs);
}
Assertions.checkNotNull(trackOutputs.get(trackIndex))
.sampleMetadata(timeUs, flags, size, offset, toExoPlayerCryptoData(trackIndex, cryptoInfo));
}
// Private methods.
private boolean maybeObtainChunkIndex(MediaFormat mediaFormat) {
@Nullable
ByteBuffer chunkIndexSizesByteBuffer =
mediaFormat.getByteBuffer(MEDIA_FORMAT_KEY_CHUNK_INDEX_SIZES);
if (chunkIndexSizesByteBuffer == null) {
return false;
}
IntBuffer chunkIndexSizes = chunkIndexSizesByteBuffer.asIntBuffer();
LongBuffer chunkIndexOffsets =
Assertions.checkNotNull(mediaFormat.getByteBuffer(MEDIA_FORMAT_KEY_CHUNK_INDEX_OFFSETS))
.asLongBuffer();
LongBuffer chunkIndexDurationsUs =
Assertions.checkNotNull(mediaFormat.getByteBuffer(MEDIA_FORMAT_KEY_CHUNK_INDEX_DURATIONS))
.asLongBuffer();
LongBuffer chunkIndexTimesUs =
Assertions.checkNotNull(mediaFormat.getByteBuffer(MEDIA_FORMAT_KEY_CHUNK_INDEX_TIMES))
.asLongBuffer();
int[] sizes = new int[chunkIndexSizes.remaining()];
long[] offsets = new long[chunkIndexOffsets.remaining()];
long[] durationsUs = new long[chunkIndexDurationsUs.remaining()];
long[] timesUs = new long[chunkIndexTimesUs.remaining()];
chunkIndexSizes.get(sizes);
chunkIndexOffsets.get(offsets);
chunkIndexDurationsUs.get(durationsUs);
chunkIndexTimesUs.get(timesUs);
lastChunkIndex = new ChunkIndex(sizes, offsets, durationsUs, timesUs);
extractorOutput.seekMap(lastChunkIndex);
return true;
}
private void ensureSpaceForTrackIndex(int trackIndex) {
for (int i = trackOutputs.size(); i <= trackIndex; i++) {
trackOutputs.add(null);
trackFormats.add(null);
lastReceivedCryptoInfos.add(null);
lastOutputCryptoDatas.add(null);
}
}
@Nullable
private CryptoData toExoPlayerCryptoData(int trackIndex, @Nullable CryptoInfo cryptoInfo) {
if (cryptoInfo == null) {
return null;
}
@Nullable CryptoInfo lastReceivedCryptoInfo = lastReceivedCryptoInfos.get(trackIndex);
CryptoData cryptoDataToOutput;
// MediaParser keeps identity and value equality aligned for efficient comparison.
if (lastReceivedCryptoInfo == cryptoInfo) {
// They match, we can reuse the last one we created.
cryptoDataToOutput = Assertions.checkNotNull(lastOutputCryptoDatas.get(trackIndex));
} else {
// They don't match, we create a new CryptoData.
// TODO: Access pattern encryption info directly once the Android SDK makes it visible.
// See [Internal ref: b/154248283].
int encryptedBlocks;
int clearBlocks;
try {
Matcher matcher = REGEX_CRYPTO_INFO_PATTERN.matcher(cryptoInfo.toString());
matcher.find();
encryptedBlocks = Integer.parseInt(Util.castNonNull(matcher.group(1)));
clearBlocks = Integer.parseInt(Util.castNonNull(matcher.group(2)));
} catch (RuntimeException e) {
// Should never happen.
Log.e(TAG, "Unexpected error while parsing CryptoInfo: " + cryptoInfo, e);
// Assume no-pattern encryption.
encryptedBlocks = 0;
clearBlocks = 0;
}
cryptoDataToOutput =
new CryptoData(cryptoInfo.mode, cryptoInfo.key, encryptedBlocks, clearBlocks);
lastReceivedCryptoInfos.set(trackIndex, cryptoInfo);
lastOutputCryptoDatas.set(trackIndex, cryptoDataToOutput);
}
return cryptoDataToOutput;
}
private void maybeEndTracks() {
if (!tracksFoundCalled || tracksEnded) {
return;
}
int size = trackOutputs.size();
for (int i = 0; i < size; i++) {
if (trackOutputs.get(i) == null) {
return;
}
}
extractorOutput.endTracks();
tracksEnded = true;
}
private static int toTrackTypeConstant(@Nullable String string) {
if (string == null) {
return C.TRACK_TYPE_UNKNOWN;
}
switch (string) {
case "audio":
return C.TRACK_TYPE_AUDIO;
case "video":
return C.TRACK_TYPE_VIDEO;
case "text":
return C.TRACK_TYPE_TEXT;
case "metadata":
return C.TRACK_TYPE_METADATA;
case "unknown":
return C.TRACK_TYPE_UNKNOWN;
default:
// Must be a MIME type.
return MimeTypes.getTrackType(string);
}
}
private Format toExoPlayerFormat(TrackData trackData) {
// TODO: Consider adding support for the following:
// format.id
// format.stereoMode
// format.projectionData
MediaFormat mediaFormat = trackData.mediaFormat;
@Nullable String mediaFormatMimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
int mediaFormatAccessibilityChannel =
mediaFormat.getInteger(
MediaFormat.KEY_CAPTION_SERVICE_NUMBER, /* defaultValue= */ Format.NO_VALUE);
Format.Builder formatBuilder =
new Format.Builder()
.setDrmInitData(
toExoPlayerDrmInitData(
mediaFormat.getString("crypto-mode-fourcc"), trackData.drmInitData))
.setContainerMimeType(containerMimeType)
.setPeakBitrate(
mediaFormat.getInteger(
MediaFormat.KEY_BIT_RATE, /* defaultValue= */ Format.NO_VALUE))
.setChannelCount(
mediaFormat.getInteger(
MediaFormat.KEY_CHANNEL_COUNT, /* defaultValue= */ Format.NO_VALUE))
.setColorInfo(getColorInfo(mediaFormat))
.setSampleMimeType(mediaFormatMimeType)
.setCodecs(mediaFormat.getString(MediaFormat.KEY_CODECS_STRING))
.setFrameRate(
mediaFormat.getFloat(
MediaFormat.KEY_FRAME_RATE, /* defaultValue= */ Format.NO_VALUE))
.setWidth(
mediaFormat.getInteger(MediaFormat.KEY_WIDTH, /* defaultValue= */ Format.NO_VALUE))
.setHeight(
mediaFormat.getInteger(MediaFormat.KEY_HEIGHT, /* defaultValue= */ Format.NO_VALUE))
.setInitializationData(getInitializationData(mediaFormat))
.setLanguage(mediaFormat.getString(MediaFormat.KEY_LANGUAGE))
.setMaxInputSize(
mediaFormat.getInteger(
MediaFormat.KEY_MAX_INPUT_SIZE, /* defaultValue= */ Format.NO_VALUE))
.setPcmEncoding(
mediaFormat.getInteger("exo-pcm-encoding", /* defaultValue= */ Format.NO_VALUE))
.setRotationDegrees(
mediaFormat.getInteger(MediaFormat.KEY_ROTATION, /* defaultValue= */ 0))
.setSampleRate(
mediaFormat.getInteger(
MediaFormat.KEY_SAMPLE_RATE, /* defaultValue= */ Format.NO_VALUE))
.setSelectionFlags(getSelectionFlags(mediaFormat))
.setEncoderDelay(
mediaFormat.getInteger(MediaFormat.KEY_ENCODER_DELAY, /* defaultValue= */ 0))
.setEncoderPadding(
mediaFormat.getInteger(MediaFormat.KEY_ENCODER_PADDING, /* defaultValue= */ 0))
.setPixelWidthHeightRatio(
mediaFormat.getFloat("pixel-width-height-ratio-float", /* defaultValue= */ 1f))
.setSubsampleOffsetUs(
mediaFormat.getLong(
"subsample-offset-us-long", /* defaultValue= */ Format.OFFSET_SAMPLE_RELATIVE))
.setAccessibilityChannel(mediaFormatAccessibilityChannel);
for (int i = 0; i < muxedCaptionFormats.size(); i++) {
Format muxedCaptionFormat = muxedCaptionFormats.get(i);
if (Util.areEqual(muxedCaptionFormat.sampleMimeType, mediaFormatMimeType)
&& muxedCaptionFormat.accessibilityChannel == mediaFormatAccessibilityChannel) {
// The track's format matches this muxedCaptionFormat, so we apply the manifest format
// information to the track.
formatBuilder
.setLanguage(muxedCaptionFormat.language)
.setRoleFlags(muxedCaptionFormat.roleFlags)
.setSelectionFlags(muxedCaptionFormat.selectionFlags)
.setLabel(muxedCaptionFormat.label)
.setMetadata(muxedCaptionFormat.metadata);
break;
}
}
return formatBuilder.build();
}
@Nullable
private static DrmInitData toExoPlayerDrmInitData(
@Nullable String schemeType, @Nullable android.media.DrmInitData drmInitData) {
if (drmInitData == null) {
return null;
}
SchemeData[] schemeDatas = new SchemeData[drmInitData.getSchemeInitDataCount()];
for (int i = 0; i < schemeDatas.length; i++) {
SchemeInitData schemeInitData = drmInitData.getSchemeInitDataAt(i);
schemeDatas[i] =
new SchemeData(schemeInitData.uuid, schemeInitData.mimeType, schemeInitData.data);
}
return new DrmInitData(schemeType, schemeDatas);
}
@SelectionFlags
private static int getSelectionFlags(MediaFormat mediaFormat) {
int selectionFlags = 0;
selectionFlags |=
getFlag(
mediaFormat,
/* key= */ MediaFormat.KEY_IS_AUTOSELECT,
/* returnValueIfPresent= */ C.SELECTION_FLAG_AUTOSELECT);
selectionFlags |=
getFlag(
mediaFormat,
/* key= */ MediaFormat.KEY_IS_DEFAULT,
/* returnValueIfPresent= */ C.SELECTION_FLAG_DEFAULT);
selectionFlags |=
getFlag(
mediaFormat,
/* key= */ MediaFormat.KEY_IS_FORCED_SUBTITLE,
/* returnValueIfPresent= */ C.SELECTION_FLAG_FORCED);
return selectionFlags;
}
private static int getFlag(MediaFormat mediaFormat, String key, int returnValueIfPresent) {
return mediaFormat.getInteger(key, /* defaultValue= */ 0) != 0 ? returnValueIfPresent : 0;
}
private static List<byte[]> getInitializationData(MediaFormat mediaFormat) {
ArrayList<byte[]> initData = new ArrayList<>();
int i = 0;
while (true) {
@Nullable ByteBuffer byteBuffer = mediaFormat.getByteBuffer("csd-" + i++);
if (byteBuffer == null) {
break;
}
initData.add(getArray(byteBuffer));
}
return initData;
}
@Nullable
private static ColorInfo getColorInfo(MediaFormat mediaFormat) {
@Nullable
ByteBuffer hdrStaticInfoByteBuffer = mediaFormat.getByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO);
@Nullable
byte[] hdrStaticInfo =
hdrStaticInfoByteBuffer != null ? getArray(hdrStaticInfoByteBuffer) : null;
int colorTransfer =
mediaFormat.getInteger(MediaFormat.KEY_COLOR_TRANSFER, /* defaultValue= */ Format.NO_VALUE);
int colorRange =
mediaFormat.getInteger(MediaFormat.KEY_COLOR_RANGE, /* defaultValue= */ Format.NO_VALUE);
int colorStandard =
mediaFormat.getInteger(MediaFormat.KEY_COLOR_STANDARD, /* defaultValue= */ Format.NO_VALUE);
if (hdrStaticInfo != null
|| colorTransfer != Format.NO_VALUE
|| colorRange != Format.NO_VALUE
|| colorStandard != Format.NO_VALUE) {
return new ColorInfo(colorStandard, colorRange, colorTransfer, hdrStaticInfo);
}
return null;
}
private static byte[] getArray(ByteBuffer byteBuffer) {
byte[] array = new byte[byteBuffer.remaining()];
byteBuffer.get(array);
return array;
}
private static String getMimeType(String parserName) {
switch (parserName) {
case PARSER_NAME_MATROSKA:
return MimeTypes.VIDEO_WEBM;
case PARSER_NAME_FMP4:
case PARSER_NAME_MP4:
return MimeTypes.VIDEO_MP4;
case PARSER_NAME_MP3:
return MimeTypes.AUDIO_MPEG;
case PARSER_NAME_ADTS:
return MimeTypes.AUDIO_AAC;
case PARSER_NAME_AC3:
return MimeTypes.AUDIO_AC3;
case PARSER_NAME_TS:
return MimeTypes.VIDEO_MP2T;
case PARSER_NAME_FLV:
return MimeTypes.VIDEO_FLV;
case PARSER_NAME_OGG:
return MimeTypes.AUDIO_OGG;
case PARSER_NAME_PS:
return MimeTypes.VIDEO_PS;
case PARSER_NAME_WAV:
return MimeTypes.AUDIO_RAW;
case PARSER_NAME_AMR:
return MimeTypes.AUDIO_AMR;
case PARSER_NAME_AC4:
return MimeTypes.AUDIO_AC4;
case PARSER_NAME_FLAC:
return MimeTypes.AUDIO_FLAC;
default:
throw new IllegalArgumentException("Illegal parser name: " + parserName);
}
}
private static final class SeekMapAdapter implements SeekMap {
private final MediaParser.SeekMap adaptedSeekMap;
public SeekMapAdapter(MediaParser.SeekMap adaptedSeekMap) {
this.adaptedSeekMap = adaptedSeekMap;
}
@Override
public boolean isSeekable() {
return adaptedSeekMap.isSeekable();
}
@Override
public long getDurationUs() {
long durationMicros = adaptedSeekMap.getDurationMicros();
return durationMicros != MediaParser.SeekMap.UNKNOWN_DURATION ? durationMicros : C.TIME_UNSET;
}
@Override
@SuppressWarnings("ReferenceEquality")
public SeekPoints getSeekPoints(long timeUs) {
Pair<MediaParser.SeekPoint, MediaParser.SeekPoint> seekPoints =
adaptedSeekMap.getSeekPoints(timeUs);
SeekPoints exoPlayerSeekPoints;
if (seekPoints.first == seekPoints.second) {
exoPlayerSeekPoints = new SeekPoints(asExoPlayerSeekPoint(seekPoints.first));
} else {
exoPlayerSeekPoints =
new SeekPoints(
asExoPlayerSeekPoint(seekPoints.first), asExoPlayerSeekPoint(seekPoints.second));
}
return exoPlayerSeekPoints;
}
private static SeekPoint asExoPlayerSeekPoint(MediaParser.SeekPoint seekPoint) {
return new SeekPoint(seekPoint.timeMicros, seekPoint.position);
}
}
private static final class DataReaderAdapter implements DataReader {
@Nullable public MediaParser.InputReader input;
@Override
public int read(byte[] target, int offset, int length) throws IOException {
return Util.castNonNull(input).read(target, offset, length);
}
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@NonNullApi
package com.google.android.exoplayer2.source.mediaparser;
import com.google.android.exoplayer2.util.NonNullApi;

View File

@ -804,7 +804,6 @@ public class DefaultDashChunkSource implements DashChunkSource {
List<Format> closedCaptionFormats,
@Nullable TrackOutput playerEmsgTrackOutput) {
String containerMimeType = representation.format.containerMimeType;
Extractor extractor;
if (MimeTypes.isText(containerMimeType)) {
if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {

View File

@ -52,7 +52,6 @@ import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@ -546,5 +545,4 @@ public final class HlsMediaSource extends BaseMediaSource
}
refreshSourceInfo(timeline);
}
}

View File

@ -0,0 +1,281 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.hls;
import static android.media.MediaParser.PARAMETER_TS_IGNORE_AAC_STREAM;
import static android.media.MediaParser.PARAMETER_TS_IGNORE_AVC_STREAM;
import static android.media.MediaParser.PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM;
import static android.media.MediaParser.PARAMETER_TS_MODE;
import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.PARAMETER_EAGERLY_EXPOSE_TRACK_TYPE;
import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.PARAMETER_EXPOSE_CAPTION_FORMATS;
import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.PARAMETER_IGNORE_TIMESTAMP_OFFSET;
import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.PARAMETER_IN_BAND_CRYPTO_INFO;
import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS;
import android.annotation.SuppressLint;
import android.media.MediaFormat;
import android.media.MediaParser;
import android.media.MediaParser.OutputConsumer;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.source.mediaparser.InputReaderAdapterV30;
import com.google.android.exoplayer2.source.mediaparser.MediaParserUtil;
import com.google.android.exoplayer2.source.mediaparser.OutputConsumerAdapterV30;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FileTypes;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
/** {@link HlsMediaChunkExtractor} implemented on top of the platform's {@link MediaParser}. */
@RequiresApi(30)
public final class MediaParserHlsMediaChunkExtractor implements HlsMediaChunkExtractor {
/**
* {@link HlsExtractorFactory} implementation that produces {@link
* MediaParserHlsMediaChunkExtractor} for all container formats except WebVTT, for which a {@link
* BundledHlsMediaChunkExtractor} is returned.
*/
public static final HlsExtractorFactory FACTORY =
(uri,
format,
muxedCaptionFormats,
timestampAdjuster,
responseHeaders,
sniffingExtractorInput) -> {
if (FileTypes.inferFileTypeFromMimeType(format.sampleMimeType) == FileTypes.WEBVTT) {
// The segment contains WebVTT. MediaParser does not support WebVTT parsing, so we use the
// bundled extractor.
return new BundledHlsMediaChunkExtractor(
new WebvttExtractor(format.language, timestampAdjuster), format, timestampAdjuster);
}
boolean overrideInBandCaptionDeclarations = muxedCaptionFormats != null;
ImmutableList.Builder<MediaFormat> muxedCaptionMediaFormatsBuilder =
ImmutableList.builder();
if (muxedCaptionFormats != null) {
// The manifest contains captions declarations. We use those to determine which captions
// will be exposed by MediaParser.
for (int i = 0; i < muxedCaptionFormats.size(); i++) {
muxedCaptionMediaFormatsBuilder.add(
MediaParserUtil.toCaptionsMediaFormat(muxedCaptionFormats.get(i)));
}
} else {
// The manifest does not declare any captions in the stream. Imitate the default HLS
// extractor factory and declare a 608 track by default.
muxedCaptionMediaFormatsBuilder.add(
MediaParserUtil.toCaptionsMediaFormat(
new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_CEA608).build()));
}
ImmutableList<MediaFormat> muxedCaptionMediaFormats =
muxedCaptionMediaFormatsBuilder.build();
// TODO: Factor out code for optimizing the sniffing order across both factories.
OutputConsumerAdapterV30 outputConsumerAdapter = new OutputConsumerAdapterV30();
outputConsumerAdapter.setMuxedCaptionFormats(
muxedCaptionFormats != null ? muxedCaptionFormats : ImmutableList.of());
outputConsumerAdapter.setTimestampAdjuster(timestampAdjuster);
MediaParser mediaParser =
createMediaParserInstance(
outputConsumerAdapter,
format,
overrideInBandCaptionDeclarations,
muxedCaptionMediaFormats,
MediaParser.PARSER_NAME_FMP4,
MediaParser.PARSER_NAME_AC3,
MediaParser.PARSER_NAME_AC4,
MediaParser.PARSER_NAME_ADTS,
MediaParser.PARSER_NAME_MP3,
MediaParser.PARSER_NAME_TS);
PeekingInputReader peekingInputReader = new PeekingInputReader(sniffingExtractorInput);
// The chunk extractor constructor requires an instance with a known parser name, so we
// advance once for MediaParser to sniff the content.
mediaParser.advance(peekingInputReader);
outputConsumerAdapter.setSelectedParserName(mediaParser.getParserName());
return new MediaParserHlsMediaChunkExtractor(
mediaParser,
outputConsumerAdapter,
format,
overrideInBandCaptionDeclarations,
muxedCaptionMediaFormats,
/* leadingBytesToSkip= */ peekingInputReader.totalPeekedBytes);
};
private final OutputConsumerAdapterV30 outputConsumerAdapter;
private final InputReaderAdapterV30 inputReaderAdapter;
private final MediaParser mediaParser;
private final Format format;
private final boolean overrideInBandCaptionDeclarations;
private final ImmutableList<MediaFormat> muxedCaptionMediaFormats;
private int pendingSkipBytes;
/**
* Creates a new instance.
*
* @param mediaParser The {@link MediaParser} instance to use for extraction of segments. The
* provided instance must have completed sniffing, or must have been created by name.
* @param outputConsumerAdapter The {@link OutputConsumerAdapterV30} with which {@code
* mediaParser} was created.
* @param format The {@link Format} associated with the segment.
* @param overrideInBandCaptionDeclarations Whether to ignore any in-band caption track
* declarations in favor of using the {@code muxedCaptionMediaFormats} instead. If false,
* caption declarations found in the extracted media will be used, causing {@code
* muxedCaptionMediaFormats} to be ignored instead.
* @param muxedCaptionMediaFormats The list of in-band caption {@link MediaFormat MediaFormats}
* that {@link MediaParser} should expose.
* @param leadingBytesToSkip The number of bytes to skip from the start of the input before
* starting extraction.
*/
public MediaParserHlsMediaChunkExtractor(
MediaParser mediaParser,
OutputConsumerAdapterV30 outputConsumerAdapter,
Format format,
boolean overrideInBandCaptionDeclarations,
ImmutableList<MediaFormat> muxedCaptionMediaFormats,
int leadingBytesToSkip) {
this.mediaParser = mediaParser;
this.outputConsumerAdapter = outputConsumerAdapter;
this.overrideInBandCaptionDeclarations = overrideInBandCaptionDeclarations;
this.muxedCaptionMediaFormats = muxedCaptionMediaFormats;
this.format = format;
pendingSkipBytes = leadingBytesToSkip;
inputReaderAdapter = new InputReaderAdapterV30();
}
// ChunkExtractor implementation.
@Override
public void init(ExtractorOutput extractorOutput) {
outputConsumerAdapter.setExtractorOutput(extractorOutput);
}
@Override
public boolean read(ExtractorInput extractorInput) throws IOException {
extractorInput.skipFully(pendingSkipBytes);
pendingSkipBytes = 0;
inputReaderAdapter.setDataReader(extractorInput, extractorInput.getLength());
return mediaParser.advance(inputReaderAdapter);
}
@Override
public boolean isPackedAudioExtractor() {
String parserName = mediaParser.getParserName();
return MediaParser.PARSER_NAME_AC3.equals(parserName)
|| MediaParser.PARSER_NAME_AC4.equals(parserName)
|| MediaParser.PARSER_NAME_ADTS.equals(parserName)
|| MediaParser.PARSER_NAME_MP3.equals(parserName);
}
@Override
public boolean isReusable() {
String parserName = mediaParser.getParserName();
return MediaParser.PARSER_NAME_FMP4.equals(parserName)
|| MediaParser.PARSER_NAME_TS.equals(parserName);
}
@Override
public HlsMediaChunkExtractor recreate() {
Assertions.checkState(!isReusable());
return new MediaParserHlsMediaChunkExtractor(
createMediaParserInstance(
outputConsumerAdapter,
format,
overrideInBandCaptionDeclarations,
muxedCaptionMediaFormats,
mediaParser.getParserName()),
outputConsumerAdapter,
format,
overrideInBandCaptionDeclarations,
muxedCaptionMediaFormats,
/* leadingBytesToSkip= */ 0);
}
// Allow constants that are not part of the public MediaParser API.
@SuppressLint({"WrongConstant"})
private static MediaParser createMediaParserInstance(
OutputConsumer outputConsumer,
Format format,
boolean overrideInBandCaptionDeclarations,
ImmutableList<MediaFormat> muxedCaptionMediaFormats,
String... parserNames) {
MediaParser mediaParser =
parserNames.length == 1
? MediaParser.createByName(parserNames[0], outputConsumer)
: MediaParser.create(outputConsumer, parserNames);
mediaParser.setParameter(PARAMETER_EXPOSE_CAPTION_FORMATS, muxedCaptionMediaFormats);
mediaParser.setParameter(
PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS, overrideInBandCaptionDeclarations);
mediaParser.setParameter(PARAMETER_IN_BAND_CRYPTO_INFO, true);
mediaParser.setParameter(PARAMETER_EAGERLY_EXPOSE_TRACK_TYPE, true);
mediaParser.setParameter(PARAMETER_IGNORE_TIMESTAMP_OFFSET, true);
mediaParser.setParameter(PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM, true);
mediaParser.setParameter(PARAMETER_TS_MODE, "hls");
@Nullable String codecs = format.codecs;
if (!TextUtils.isEmpty(codecs)) {
// Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
// exist. If we know from the codec attribute that they don't exist, then we can
// explicitly ignore them even if they're declared.
if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {
mediaParser.setParameter(PARAMETER_TS_IGNORE_AAC_STREAM, true);
}
if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
mediaParser.setParameter(PARAMETER_TS_IGNORE_AVC_STREAM, true);
}
}
return mediaParser;
}
private static final class PeekingInputReader implements MediaParser.SeekableInputReader {
private final ExtractorInput extractorInput;
private int totalPeekedBytes;
private PeekingInputReader(ExtractorInput extractorInput) {
this.extractorInput = extractorInput;
}
@Override
public int read(@NonNull byte[] buffer, int offset, int readLength) throws IOException {
int peekedBytes = extractorInput.peek(buffer, offset, readLength);
totalPeekedBytes += peekedBytes;
return peekedBytes;
}
@Override
public long getPosition() {
return extractorInput.getPeekPosition();
}
@Override
public long getLength() {
return extractorInput.getLength();
}
@Override
public void seekToPosition(long position) {
// Seeking is not allowed when sniffing the content.
throw new UnsupportedOperationException();
}
}
}