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