/*
* Copyright (C) 2014 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.exoplayer.extractor;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.SampleSource.SampleSourceReader;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.Allocator;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.DefaultAllocator;
import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
import android.net.Uri;
import android.os.SystemClock;
import android.util.SparseArray;
import java.io.EOFException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* A {@link SampleSource} that extracts sample data using an {@link Extractor}.
*
* <p>If no {@link Extractor} instances are passed to the constructor, the input stream container
* format will be detected automatically from the following supported formats:
*
* <ul>
* <li>Fragmented MP4
* ({@link com.google.android.exoplayer.extractor.mp4.FragmentedMp4Extractor})</li>
* <li>Unfragmented MP4, including M4A
* ({@link com.google.android.exoplayer.extractor.mp4.Mp4Extractor})</li>
* <li>Matroska, including WebM
* ({@link com.google.android.exoplayer.extractor.webm.WebmExtractor})</li>
* <li>MP3 ({@link com.google.android.exoplayer.extractor.mp3.Mp3Extractor})</li>
* <li>AAC ({@link com.google.android.exoplayer.extractor.ts.AdtsExtractor})</li>
* <li>MPEG TS ({@link com.google.android.exoplayer.extractor.ts.TsExtractor}</li>
* </ul>
*
* <p>Seeking in AAC and MPEG TS streams is not supported.
*
* <p>To override the default extractors, pass one or more {@link Extractor} instances to the
* constructor. When reading a new stream, the first {@link Extractor} that returns {@code true}
* from {@link Extractor#sniff(ExtractorInput)} will be used.
*/
public final class ExtractorSampleSource implements SampleSource, SampleSourceReader,
ExtractorOutput, Loader.Callback {
/**
* Thrown if the input format could not recognized.
*/
public static final class UnrecognizedInputFormatException extends ParserException {
public UnrecognizedInputFormatException(Extractor[] extractors) {
super("None of the available extractors ("
+ Util.getCommaDelimitedSimpleClassNames(extractors) + ") could read the stream.");
}
}
/**
* The default minimum number of times to retry loading prior to failing for on-demand streams.
*/
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND = 3;
/**
* The default minimum number of times to retry loading prior to failing for live streams.
*/
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE = 6;
private static final int MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA = -1;
private static final long NO_RESET_PENDING = Long.MIN_VALUE;
/**
* Default extractor classes in priority order. They are referred to indirectly so that it is
* possible to remove unused extractors.
*/
private static final List<Class<? extends Extractor>> DEFAULT_EXTRACTOR_CLASSES;
static {
DEFAULT_EXTRACTOR_CLASSES = new ArrayList<>();
// Load extractors using reflection so that they can be deleted cleanly.
// Class.forName(<class name>) appears for each extractor so that automated tools like proguard
// can detect the use of reflection (see http://proguard.sourceforge.net/FAQ.html#forname).
try {
DEFAULT_EXTRACTOR_CLASSES.add(
Class.forName("com.google.android.exoplayer.extractor.webm.WebmExtractor")
.asSubclass(Extractor.class));
} catch (ClassNotFoundException e) {
// Extractor not found.
}
try {
DEFAULT_EXTRACTOR_CLASSES.add(
Class.forName("com.google.android.exoplayer.extractor.mp4.FragmentedMp4Extractor")
.asSubclass(Extractor.class));
} catch (ClassNotFoundException e) {
// Extractor not found.
}
try {
DEFAULT_EXTRACTOR_CLASSES.add(
Class.forName("com.google.android.exoplayer.extractor.mp4.Mp4Extractor")
.asSubclass(Extractor.class));
} catch (ClassNotFoundException e) {
// Extractor not found.
}
try {
DEFAULT_EXTRACTOR_CLASSES.add(
Class.forName("com.google.android.exoplayer.extractor.mp3.Mp3Extractor")
.asSubclass(Extractor.class));
} catch (ClassNotFoundException e) {
// Extractor not found.
}
try {
DEFAULT_EXTRACTOR_CLASSES.add(
Class.forName("com.google.android.exoplayer.extractor.ts.AdtsExtractor")
.asSubclass(Extractor.class));
} catch (ClassNotFoundException e) {
// Extractor not found.
}
try {
DEFAULT_EXTRACTOR_CLASSES.add(
Class.forName("com.google.android.exoplayer.extractor.ts.TsExtractor")
.asSubclass(Extractor.class));
} catch (ClassNotFoundException e) {
// Extractor not found.
}
}
private final ExtractorHolder extractorHolder;
private final Allocator allocator;
private final int requestedBufferSize;
private final SparseArray<InternalTrackOutput> sampleQueues;
private final int minLoadableRetryCount;
private final Uri uri;
private final DataSource dataSource;
private volatile boolean tracksBuilt;
private volatile SeekMap seekMap;
private volatile DrmInitData drmInitData;
private boolean prepared;
private int enabledTrackCount;
private MediaFormat[] mediaFormats;
private long maxTrackDurationUs;
private boolean[] pendingMediaFormat;
private boolean[] pendingDiscontinuities;
private boolean[] trackEnabledStates;
private int remainingReleaseCount;
private long downstreamPositionUs;
private long lastSeekPositionUs;
private long pendingResetPositionUs;
private boolean havePendingNextSampleUs;
private long pendingNextSampleUs;
private long sampleTimeOffsetUs;
private Loader loader;
private ExtractingLoadable loadable;
private IOException currentLoadableException;
// TODO: Set this back to 0 in the correct place (some place indicative of making progress).
private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp;
private boolean loadingFinished;
private int extractedSampleCount;
private int extractedSampleCountAtStartOfLoad;
/**
* @param uri The {@link Uri} of the media stream.
* @param dataSource A data source to read the media stream.
* @param requestedBufferSize The requested total buffer size for storing sample data, in bytes.
* The actual allocated size may exceed the value passed in if the implementation requires it.
* @param extractors {@link Extractor}s to extract the media stream, in order of decreasing
* priority. If omitted, the default extractors will be used.
*/
@Deprecated
public ExtractorSampleSource(Uri uri, DataSource dataSource, int requestedBufferSize,
Extractor... extractors) {
this(uri, dataSource, new DefaultAllocator(64 * 1024), requestedBufferSize, extractors);
}
/**
* @param uri The {@link Uri} of the media stream.
* @param dataSource A data source to read the media stream.
* @param allocator An {@link Allocator} from which to obtain memory allocations.
* @param requestedBufferSize The requested total buffer size for storing sample data, in bytes.
* The actual allocated size may exceed the value passed in if the implementation requires it.
* @param extractors {@link Extractor}s to extract the media stream, in order of decreasing
* priority. If omitted, the default extractors will be used.
*/
public ExtractorSampleSource(Uri uri, DataSource dataSource, Allocator allocator,
int requestedBufferSize, Extractor... extractors) {
this(uri, dataSource, allocator, requestedBufferSize, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA,
extractors);
}
/**
* @param uri The {@link Uri} of the media stream.
* @param dataSource A data source to read the media stream.
* @param requestedBufferSize The requested total buffer size for storing sample data, in bytes.
* The actual allocated size may exceed the value passed in if the implementation requires it.
* @param minLoadableRetryCount The minimum number of times that the sample source will retry
* if a loading error occurs.
* @param extractors {@link Extractor}s to extract the media stream, in order of decreasing
* priority. If omitted, the default extractors will be used.
*/
@Deprecated
public ExtractorSampleSource(Uri uri, DataSource dataSource, int requestedBufferSize,
int minLoadableRetryCount, Extractor... extractors) {
this(uri, dataSource, new DefaultAllocator(64 * 1024), requestedBufferSize,
minLoadableRetryCount, extractors);
}
/**
* @param uri The {@link Uri} of the media stream.
* @param dataSource A data source to read the media stream.
* @param allocator An {@link Allocator} from which to obtain memory allocations.
* @param requestedBufferSize The requested total buffer size for storing sample data, in bytes.
* The actual allocated size may exceed the value passed in if the implementation requires it.
* @param minLoadableRetryCount The minimum number of times that the sample source will retry
* if a loading error occurs.
* @param extractors {@link Extractor}s to extract the media stream, in order of decreasing
* priority. If omitted, the default extractors will be used.
*/
public ExtractorSampleSource(Uri uri, DataSource dataSource, Allocator allocator,
int requestedBufferSize, int minLoadableRetryCount, Extractor... extractors) {
this.uri = uri;
this.dataSource = dataSource;
this.allocator = allocator;
this.requestedBufferSize = requestedBufferSize;
this.minLoadableRetryCount = minLoadableRetryCount;
if (extractors == null || extractors.length == 0) {
extractors = new Extractor[DEFAULT_EXTRACTOR_CLASSES.size()];
for (int i = 0; i < extractors.length; i++) {
try {
extractors[i] = DEFAULT_EXTRACTOR_CLASSES.get(i).newInstance();
} catch (InstantiationException e) {
throw new IllegalStateException("Unexpected error creating default extractor", e);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unexpected error creating default extractor", e);
}
}
}
extractorHolder = new ExtractorHolder(extractors, this);
sampleQueues = new SparseArray<>();
pendingResetPositionUs = NO_RESET_PENDING;
}
@Override
public SampleSourceReader register() {
remainingReleaseCount++;
return this;
}
@Override
public boolean prepare(long positionUs) {
if (prepared) {
return true;
}
if (loader == null) {
loader = new Loader("Loader:ExtractorSampleSource");
}
maybeStartLoading();
if (seekMap != null && tracksBuilt && haveFormatsForAllTracks()) {
int trackCount = sampleQueues.size();
trackEnabledStates = new boolean[trackCount];
pendingDiscontinuities = new boolean[trackCount];
pendingMediaFormat = new boolean[trackCount];
mediaFormats = new MediaFormat[trackCount];
maxTrackDurationUs = C.UNKNOWN_TIME_US;
for (int i = 0; i < trackCount; i++) {
MediaFormat format = sampleQueues.valueAt(i).getFormat();
mediaFormats[i] = format;
if (format.durationUs != C.UNKNOWN_TIME_US && format.durationUs > maxTrackDurationUs) {
maxTrackDurationUs = format.durationUs;
}
}
prepared = true;
return true;
}
return false;
}
@Override
public int getTrackCount() {
return sampleQueues.size();
}
@Override
public MediaFormat getFormat(int track) {
Assertions.checkState(prepared);
return mediaFormats[track];
}
@Override
public void enable(int track, long positionUs) {
Assertions.checkState(prepared);
Assertions.checkState(!trackEnabledStates[track]);
enabledTrackCount++;
trackEnabledStates[track] = true;
pendingMediaFormat[track] = true;
pendingDiscontinuities[track] = false;
if (enabledTrackCount == 1) {
// Treat all enables in non-seekable media as being from t=0.
positionUs = !seekMap.isSeekable() ? 0 : positionUs;
downstreamPositionUs = positionUs;
lastSeekPositionUs = positionUs;
restartFrom(positionUs);
}
}
@Override
public void disable(int track) {
Assertions.checkState(prepared);
Assertions.checkState(trackEnabledStates[track]);
enabledTrackCount--;
trackEnabledStates[track] = false;
if (enabledTrackCount == 0) {
downstreamPositionUs = Long.MIN_VALUE;
if (loader.isLoading()) {
loader.cancelLoading();
} else {
clearState();
allocator.trim(0);
}
}
}
@Override
public boolean continueBuffering(int track, long playbackPositionUs) {
Assertions.checkState(prepared);
Assertions.checkState(trackEnabledStates[track]);
downstreamPositionUs = playbackPositionUs;
discardSamplesForDisabledTracks(downstreamPositionUs);
if (loadingFinished) {
return true;
}
maybeStartLoading();
if (isPendingReset()) {
return false;
}
return !sampleQueues.valueAt(track).isEmpty();
}
@Override
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) {
downstreamPositionUs = playbackPositionUs;
if (pendingDiscontinuities[track]) {
pendingDiscontinuities[track] = false;
return DISCONTINUITY_READ;
}
if (onlyReadDiscontinuity || isPendingReset()) {
return NOTHING_READ;
}
InternalTrackOutput sampleQueue = sampleQueues.valueAt(track);
if (pendingMediaFormat[track]) {
formatHolder.format = sampleQueue.getFormat();
formatHolder.drmInitData = drmInitData;
pendingMediaFormat[track] = false;
return FORMAT_READ;
}
if (sampleQueue.getSample(sampleHolder)) {
boolean decodeOnly = sampleHolder.timeUs < lastSeekPositionUs;
sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
if (havePendingNextSampleUs) {
// Set the offset to make the timestamp of this sample equal to pendingNextSampleUs.
sampleTimeOffsetUs = pendingNextSampleUs - sampleHolder.timeUs;
havePendingNextSampleUs = false;
}
sampleHolder.timeUs += sampleTimeOffsetUs;
return SAMPLE_READ;
}
if (loadingFinished) {
return END_OF_STREAM;
}
return NOTHING_READ;
}
@Override
public void maybeThrowError() throws IOException {
if (currentLoadableException == null) {
return;
}
if (isCurrentLoadableExceptionFatal()) {
throw currentLoadableException;
}
int minLoadableRetryCountForMedia;
if (minLoadableRetryCount != MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA) {
minLoadableRetryCountForMedia = minLoadableRetryCount;
} else {
minLoadableRetryCountForMedia = seekMap != null && !seekMap.isSeekable()
? DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE
: DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND;
}
if (currentLoadableExceptionCount > minLoadableRetryCountForMedia) {
throw currentLoadableException;
}
}
@Override
public void seekToUs(long positionUs) {
Assertions.checkState(prepared);
Assertions.checkState(enabledTrackCount > 0);
// Treat all seeks into non-seekable media as being to t=0.
positionUs = !seekMap.isSeekable() ? 0 : positionUs;
long currentPositionUs = isPendingReset() ? pendingResetPositionUs : downstreamPositionUs;
downstreamPositionUs = positionUs;
lastSeekPositionUs = positionUs;
if (currentPositionUs == positionUs) {
return;
}
// If we're not pending a reset, see if we can seek within the sample queues.
boolean seekInsideBuffer = !isPendingReset();
for (int i = 0; seekInsideBuffer && i < sampleQueues.size(); i++) {
seekInsideBuffer &= sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs);
}
// If we failed to seek within the sample queues, we need to restart.
if (!seekInsideBuffer) {
restartFrom(positionUs);
}
// Either way, we need to send discontinuities to the downstream components.
for (int i = 0; i < pendingDiscontinuities.length; i++) {
pendingDiscontinuities[i] = true;
}
}
@Override
public long getBufferedPositionUs() {
if (loadingFinished) {
return TrackRenderer.END_OF_TRACK_US;
} else if (isPendingReset()) {
return pendingResetPositionUs;
} else {
long largestParsedTimestampUs = Long.MIN_VALUE;
for (int i = 0; i < sampleQueues.size(); i++) {
largestParsedTimestampUs = Math.max(largestParsedTimestampUs,
sampleQueues.valueAt(i).getLargestParsedTimestampUs());
}
return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs
: largestParsedTimestampUs;
}
}
@Override
public void release() {
Assertions.checkState(remainingReleaseCount > 0);
if (--remainingReleaseCount == 0 && loader != null) {
loader.release();
loader = null;
}
}
// Loader.Callback implementation.
@Override
public void onLoadCompleted(Loadable loadable) {
loadingFinished = true;
}
@Override
public void onLoadCanceled(Loadable loadable) {
if (enabledTrackCount > 0) {
restartFrom(pendingResetPositionUs);
} else {
clearState();
allocator.trim(0);
}
}
@Override
public void onLoadError(Loadable ignored, IOException e) {
currentLoadableException = e;
currentLoadableExceptionCount = extractedSampleCount > extractedSampleCountAtStartOfLoad ? 1
: currentLoadableExceptionCount + 1;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
maybeStartLoading();
}
// ExtractorOutput implementation.
@Override
public TrackOutput track(int id) {
InternalTrackOutput sampleQueue = sampleQueues.get(id);
if (sampleQueue == null) {
sampleQueue = new InternalTrackOutput(allocator);
sampleQueues.put(id, sampleQueue);
}
return sampleQueue;
}
@Override
public void endTracks() {
tracksBuilt = true;
}
@Override
public void seekMap(SeekMap seekMap) {
this.seekMap = seekMap;
}
@Override
public void drmInitData(DrmInitData drmInitData) {
this.drmInitData = drmInitData;
}
// Internal stuff.
private void restartFrom(long positionUs) {
pendingResetPositionUs = positionUs;
loadingFinished = false;
if (loader.isLoading()) {
loader.cancelLoading();
} else {
clearState();
maybeStartLoading();
}
}
private void maybeStartLoading() {
if (loadingFinished || loader.isLoading()) {
return;
}
if (currentLoadableException != null) {
if (isCurrentLoadableExceptionFatal()) {
return;
}
Assertions.checkState(loadable != null);
long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp;
if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
currentLoadableException = null;
if (!prepared) {
// We don't know whether we're playing an on-demand or a live stream. For a live stream
// we need to load from the start, as outlined below. Since we might be playing a live
// stream, play it safe and load from the start.
for (int i = 0; i < sampleQueues.size(); i++) {
sampleQueues.valueAt(i).clear();
}
loadable = createLoadableFromStart();
} else if (!seekMap.isSeekable() && maxTrackDurationUs == C.UNKNOWN_TIME_US) {
// We're playing a non-seekable stream with unknown duration. Assume it's live, and
// therefore that the data at the uri is a continuously shifting window of the latest
// available media. For this case there's no way to continue loading from where a previous
// load finished, so it's necessary to load from the start whenever commencing a new load.
for (int i = 0; i < sampleQueues.size(); i++) {
sampleQueues.valueAt(i).clear();
}
loadable = createLoadableFromStart();
// To avoid introducing a discontinuity, we shift the sample timestamps so that they will
// continue from the current downstream position.
pendingNextSampleUs = downstreamPositionUs;
havePendingNextSampleUs = true;
} else {
// We're playing a seekable on-demand stream. Resume the current loadable, which will
// request data starting from the point it left off.
}
extractedSampleCountAtStartOfLoad = extractedSampleCount;
loader.startLoading(loadable, this);
}
return;
}
// We're not retrying, so we're either starting a playback or responding to an explicit seek.
// In both cases sampleTimeOffsetUs should be reset to zero, and any pending adjustment to
// sample timestamps should be discarded.
sampleTimeOffsetUs = 0;
havePendingNextSampleUs = false;
if (!prepared) {
loadable = createLoadableFromStart();
} else {
Assertions.checkState(isPendingReset());
if (maxTrackDurationUs != C.UNKNOWN_TIME_US && pendingResetPositionUs >= maxTrackDurationUs) {
loadingFinished = true;
pendingResetPositionUs = NO_RESET_PENDING;
return;
}
loadable = createLoadableFromPositionUs(pendingResetPositionUs);
pendingResetPositionUs = NO_RESET_PENDING;
}
extractedSampleCountAtStartOfLoad = extractedSampleCount;
loader.startLoading(loadable, this);
}
private ExtractingLoadable createLoadableFromStart() {
return new ExtractingLoadable(uri, dataSource, extractorHolder, allocator, requestedBufferSize,
0);
}
private ExtractingLoadable createLoadableFromPositionUs(long positionUs) {
return new ExtractingLoadable(uri, dataSource, extractorHolder, allocator, requestedBufferSize,
seekMap.getPosition(positionUs));
}
private boolean haveFormatsForAllTracks() {
for (int i = 0; i < sampleQueues.size(); i++) {
if (!sampleQueues.valueAt(i).hasFormat()) {
return false;
}
}
return true;
}
private void discardSamplesForDisabledTracks(long timeUs) {
for (int i = 0; i < trackEnabledStates.length; i++) {
if (!trackEnabledStates[i]) {
sampleQueues.valueAt(i).discardUntil(timeUs);
}
}
}
private void clearState() {
for (int i = 0; i < sampleQueues.size(); i++) {
sampleQueues.valueAt(i).clear();
}
loadable = null;
currentLoadableException = null;
currentLoadableExceptionCount = 0;
}
private boolean isPendingReset() {
return pendingResetPositionUs != NO_RESET_PENDING;
}
private boolean isCurrentLoadableExceptionFatal() {
return currentLoadableException instanceof UnrecognizedInputFormatException;
}
private long getRetryDelayMillis(long errorCount) {
return Math.min((errorCount - 1) * 1000, 5000);
}
/**
* Extension of {@link DefaultTrackOutput} that increments a shared counter of the total number
* of extracted samples.
*/
private class InternalTrackOutput extends DefaultTrackOutput {
public InternalTrackOutput(Allocator allocator) {
super(allocator);
}
@Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
super.sampleMetadata(timeUs, flags, size, offset, encryptionKey);
extractedSampleCount++;
}
}
/**
* Loads the media stream and extracts sample data from it.
*/
private static class ExtractingLoadable implements Loadable {
private final Uri uri;
private final DataSource dataSource;
private final ExtractorHolder extractorHolder;
private final Allocator allocator;
private final int requestedBufferSize;
private final PositionHolder positionHolder;
private volatile boolean loadCanceled;
private boolean pendingExtractorSeek;
public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder,
Allocator allocator, int requestedBufferSize, long position) {
this.uri = Assertions.checkNotNull(uri);
this.dataSource = Assertions.checkNotNull(dataSource);
this.extractorHolder = Assertions.checkNotNull(extractorHolder);
this.allocator = Assertions.checkNotNull(allocator);
this.requestedBufferSize = requestedBufferSize;
positionHolder = new PositionHolder();
positionHolder.position = position;
pendingExtractorSeek = true;
}
@Override
public void cancelLoad() {
loadCanceled = true;
}
@Override
public boolean isLoadCanceled() {
return loadCanceled;
}
@Override
public void load() throws IOException, InterruptedException {
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
ExtractorInput input = null;
try {
long position = positionHolder.position;
long length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNBOUNDED, null));
if (length != C.LENGTH_UNBOUNDED) {
length += position;
}
input = new DefaultExtractorInput(dataSource, position, length);
Extractor extractor = extractorHolder.selectExtractor(input);
if (pendingExtractorSeek) {
extractor.seek();
pendingExtractorSeek = false;
}
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
allocator.blockWhileTotalBytesAllocatedExceeds(requestedBufferSize);
result = extractor.read(input, positionHolder);
// TODO: Implement throttling to stop us from buffering data too often.
}
} finally {
if (result == Extractor.RESULT_SEEK) {
result = Extractor.RESULT_CONTINUE;
} else if (input != null) {
positionHolder.position = input.getPosition();
}
dataSource.close();
}
}
}
}
/**
* Stores a list of extractors and a selected extractor when the format has been detected.
*/
private static final class ExtractorHolder {
private final Extractor[] extractors;
private final ExtractorOutput extractorOutput;
private Extractor extractor;
/**
* Creates a holder that will select an extractor and initialize it using the specified output.
*
* @param extractors One or more extractors to choose from.
* @param extractorOutput The output that will be used to initialize the selected extractor.
*/
public ExtractorHolder(Extractor[] extractors, ExtractorOutput extractorOutput) {
this.extractors = extractors;
this.extractorOutput = extractorOutput;
}
/**
* Returns an initialized extractor for reading {@code input}, and returns the same extractor on
* later calls.
*
* @param input The {@link ExtractorInput} from which data should be read.
* @throws UnrecognizedInputFormatException Thrown if the input format could not be detected.
* @throws IOException Thrown if the input could not be read.
* @throws InterruptedException Thrown if the thread was interrupted.
*/
public Extractor selectExtractor(ExtractorInput input)
throws UnrecognizedInputFormatException, IOException, InterruptedException {
if (extractor != null) {
return extractor;
}
for (Extractor extractor : extractors) {
try {
if (extractor.sniff(input)) {
this.extractor = extractor;
break;
}
} catch (EOFException e) {
// Do nothing.
}
input.resetPeekPosition();
}
if (extractor == null) {
throw new UnrecognizedInputFormatException(extractors);
}
extractor.init(extractorOutput);
return extractor;
}
}
}