From 85e0bca33d7cd4d1052a7bebfc6ab6abadc2df19 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 21 Jul 2015 17:39:38 +0100 Subject: [PATCH] Add support for choosing an extractor based on sniffing the container. - ExtractorSampleSource takes an array of extractors to test for suitability. - Extractors now implement a sniff() method that returns whether they can extract samples in the input stream's format. - Switch demo app samples to use format detection. Issue: #438 --- .../exoplayer/demo/PlayerActivity.java | 33 +-- .../android/exoplayer/demo/Samples.java | 21 +- .../demo/player/ExtractorRendererBuilder.java | 8 +- .../exoplayer/demo/webm/VideoPlayer.java | 4 +- .../exoplayer/FrameworkSampleSource.java | 18 +- .../extractor/DefaultExtractorInput.java | 105 ++++++++- .../exoplayer/extractor/Extractor.java | 9 + .../exoplayer/extractor/ExtractorInput.java | 35 ++- .../extractor/ExtractorSampleSource.java | 220 +++++++++++++++--- .../exoplayer/extractor/mp3/Mp3Extractor.java | 62 ++++- .../extractor/mp4/FragmentedMp4Extractor.java | 5 + .../exoplayer/extractor/mp4/Mp4Extractor.java | 5 + .../exoplayer/extractor/mp4/Sniffer.java | 160 +++++++++++++ .../exoplayer/extractor/ts/AdtsExtractor.java | 19 ++ .../exoplayer/extractor/ts/TsExtractor.java | 13 ++ .../exoplayer/extractor/webm/Sniffer.java | 113 +++++++++ .../extractor/webm/WebmExtractor.java | 5 + 17 files changed, 732 insertions(+), 103 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/mp4/Sniffer.java create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/webm/Sniffer.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index f54dcfae58..a075f53971 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -26,12 +26,6 @@ import com.google.android.exoplayer.demo.player.ExtractorRendererBuilder; import com.google.android.exoplayer.demo.player.HlsRendererBuilder; import com.google.android.exoplayer.demo.player.SmoothStreamingRendererBuilder; import com.google.android.exoplayer.drm.UnsupportedDrmException; -import com.google.android.exoplayer.extractor.mp3.Mp3Extractor; -import com.google.android.exoplayer.extractor.mp4.FragmentedMp4Extractor; -import com.google.android.exoplayer.extractor.mp4.Mp4Extractor; -import com.google.android.exoplayer.extractor.ts.AdtsExtractor; -import com.google.android.exoplayer.extractor.ts.TsExtractor; -import com.google.android.exoplayer.extractor.webm.WebmExtractor; import com.google.android.exoplayer.metadata.GeobMetadata; import com.google.android.exoplayer.metadata.PrivMetadata; import com.google.android.exoplayer.metadata.TxxxMetadata; @@ -84,14 +78,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, public static final int TYPE_DASH = 0; public static final int TYPE_SS = 1; public static final int TYPE_HLS = 2; - public static final int TYPE_MP4 = 3; - public static final int TYPE_MP3 = 4; - public static final int TYPE_FMP4 = 5; - public static final int TYPE_WEBM = 6; - public static final int TYPE_MKV = 7; - public static final int TYPE_TS = 8; - public static final int TYPE_AAC = 9; - public static final int TYPE_M4A = 10; + public static final int TYPE_OTHER = 3; public static final String CONTENT_TYPE_EXTRA = "content_type"; public static final String CONTENT_ID_EXTRA = "content_id"; @@ -257,22 +244,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, new WidevineTestMediaDrmCallback(contentId), audioCapabilities); case TYPE_HLS: return new HlsRendererBuilder(this, userAgent, contentUri.toString(), audioCapabilities); - case TYPE_M4A: // There are no file format differences between M4A and MP4. - case TYPE_MP4: - return new ExtractorRendererBuilder(this, userAgent, contentUri, new Mp4Extractor()); - case TYPE_MP3: - return new ExtractorRendererBuilder(this, userAgent, contentUri, new Mp3Extractor()); - case TYPE_TS: - return new ExtractorRendererBuilder(this, userAgent, contentUri, - new TsExtractor(0, audioCapabilities)); - case TYPE_AAC: - return new ExtractorRendererBuilder(this, userAgent, contentUri, new AdtsExtractor()); - case TYPE_FMP4: - return new ExtractorRendererBuilder(this, userAgent, contentUri, - new FragmentedMp4Extractor()); - case TYPE_WEBM: - case TYPE_MKV: - return new ExtractorRendererBuilder(this, userAgent, contentUri, new WebmExtractor()); + case TYPE_OTHER: + return new ExtractorRendererBuilder(this, userAgent, contentUri); default: throw new IllegalStateException("Unsupported type: " + contentType); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index 551513161f..ef54b3ebc0 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -128,28 +128,23 @@ import java.util.Locale; }; public static final Sample[] MISC = new Sample[] { - new Sample("Dizzy", "http://html5demos.com/assets/dizzy.mp4", - PlayerActivity.TYPE_MP4), + new Sample("Dizzy", "http://html5demos.com/assets/dizzy.mp4", PlayerActivity.TYPE_OTHER), new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/" - + "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac", - PlayerActivity.TYPE_AAC), + + "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac", PlayerActivity.TYPE_OTHER), new Sample("Apple TS 10s", "https://devimages.apple.com.edgekey.net/streaming/examples/" - + "bipbop_4x3/gear1/fileSequence0.ts", - PlayerActivity.TYPE_TS), + + "bipbop_4x3/gear1/fileSequence0.ts", PlayerActivity.TYPE_OTHER), new Sample("Android screens (Matroska)", "http://storage.googleapis.com/exoplayer-test-media-1/" - + "mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", PlayerActivity.TYPE_MKV), + + "mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + PlayerActivity.TYPE_OTHER), new Sample("Big Buck Bunny (MP4 Video)", "http://redirector.c.youtube.com/videoplayback?id=604ed5ce52eda7ee&itag=22&source=youtube&" + "sparams=ip,ipbits,expire,source,id&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=" + "513F28C7FDCBEC60A66C86C9A393556C99DC47FB.04C88036EEE12565A1ED864A875A58F15D8B5300" - + "&key=ik0", - PlayerActivity.TYPE_MP4), + + "&key=ik0", PlayerActivity.TYPE_OTHER), new Sample("Google Play (MP3 Audio)", - "http://storage.googleapis.com/exoplayer-test-media-0/play.mp3", - PlayerActivity.TYPE_MP3), + "http://storage.googleapis.com/exoplayer-test-media-0/play.mp3", PlayerActivity.TYPE_OTHER), new Sample("Google Glass (WebM Video with Vorbis Audio)", - "http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm", - PlayerActivity.TYPE_WEBM), + "http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm", PlayerActivity.TYPE_OTHER), }; private Samples() {} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java index 44fa221588..d45adc92c0 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java @@ -44,13 +44,11 @@ public class ExtractorRendererBuilder implements RendererBuilder { private final Context context; private final String userAgent; private final Uri uri; - private final Extractor extractor; - public ExtractorRendererBuilder(Context context, String userAgent, Uri uri, Extractor extractor) { + public ExtractorRendererBuilder(Context context, String userAgent, Uri uri) { this.context = context; this.userAgent = userAgent; this.uri = uri; - this.extractor = extractor; } @Override @@ -61,8 +59,8 @@ public class ExtractorRendererBuilder implements RendererBuilder { DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(player.getMainHandler(), null); DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); - ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, extractor, - allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE); + ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, allocator, + BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, null, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, player.getMainHandler(), player, 50); diff --git a/demo_misc/webm_sw_decoder/src/main/java/com/google/android/exoplayer/demo/webm/VideoPlayer.java b/demo_misc/webm_sw_decoder/src/main/java/com/google/android/exoplayer/demo/webm/VideoPlayer.java index 01a6a0dd5c..c855cd63ed 100644 --- a/demo_misc/webm_sw_decoder/src/main/java/com/google/android/exoplayer/demo/webm/VideoPlayer.java +++ b/demo_misc/webm_sw_decoder/src/main/java/com/google/android/exoplayer/demo/webm/VideoPlayer.java @@ -167,8 +167,8 @@ public class VideoPlayer extends Activity implements OnClickListener, ExtractorSampleSource sampleSource = new ExtractorSampleSource( Uri.fromFile(new File(filename)), new DefaultUriDataSource(this, Util.getUserAgent(this, "ExoPlayerExtWebMDemo")), - new WebmExtractor(), new DefaultAllocator(BUFFER_SEGMENT_SIZE), - BUFFER_SEGMENT_SIZE * BUFFER_SEGMENT_COUNT); + new DefaultAllocator(BUFFER_SEGMENT_SIZE), BUFFER_SEGMENT_SIZE * BUFFER_SEGMENT_COUNT, + new WebmExtractor()); TrackRenderer videoRenderer = new LibvpxVideoTrackRenderer(sampleSource, true, handler, this, 50); if (useOpenGL) { diff --git a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java index 6cf393c0e7..a319636d52 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.SampleSource.SampleSourceReader; import com.google.android.exoplayer.drm.DrmInitData; import com.google.android.exoplayer.extractor.ExtractorSampleSource; -import com.google.android.exoplayer.extractor.mp4.Mp4Extractor; import com.google.android.exoplayer.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; @@ -39,19 +38,12 @@ import java.util.UUID; *

* Warning - This class is marked as deprecated because there are known device specific issues * associated with its use, including playbacks not starting, playbacks stuttering and other - * miscellaneous failures. For mp4, m4a, mp3, webm, mpeg-ts and aac playbacks it is strongly - * recommended to use {@link ExtractorSampleSource} instead, along with the corresponding extractor - * (e.g. {@link Mp4Extractor} for mp4 playbacks). Where this is not possible this class can still be - * used, but please be aware of the associated risks. Valid use cases of this class that are not - * yet supported by {@link ExtractorSampleSource} include: - *

+ * miscellaneous failures. For mp4, m4a, mp3, webm, mkv, mpeg-ts and aac playbacks it is strongly + * recommended to use {@link ExtractorSampleSource} instead. Where this is not possible this class + * can still be used, but please be aware of the associated risks. Playing container formats for + * which an ExoPlayer extractor does not yet exist (e.g. ogg) is a valid use case of this class. *

- * Over time we hope to enhance {@link ExtractorSampleSource} to support these use cases, and hence + * Over time we hope to enhance {@link ExtractorSampleSource} to support more formats, and hence * make use of this class unnecessary. */ // TODO: This implementation needs to be fixed so that its methods are non-blocking (either diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/DefaultExtractorInput.java b/library/src/main/java/com/google/android/exoplayer/extractor/DefaultExtractorInput.java index 0fb9008afc..a7a4ae2d9b 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/DefaultExtractorInput.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/DefaultExtractorInput.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer.upstream.DataSource; import java.io.EOFException; import java.io.IOException; +import java.util.Arrays; /** * An {@link ExtractorInput} that wraps a {@link DataSource}. @@ -29,9 +30,12 @@ public final class DefaultExtractorInput implements ExtractorInput { private static final byte[] SCRATCH_SPACE = new byte[4096]; private final DataSource dataSource; + private final long streamLength; private long position; - private long length; + private byte[] peekBuffer; + private int peekBufferPosition; + private int peekBufferLength; /** * @param dataSource The wrapped {@link DataSource}. @@ -41,7 +45,8 @@ public final class DefaultExtractorInput implements ExtractorInput { public DefaultExtractorInput(DataSource dataSource, long position, long length) { this.dataSource = dataSource; this.position = position; - this.length = length; + this.streamLength = length; + peekBuffer = new byte[8 * 1024]; } @Override @@ -49,10 +54,16 @@ public final class DefaultExtractorInput implements ExtractorInput { if (Thread.interrupted()) { throw new InterruptedException(); } - int bytesRead = dataSource.read(target, offset, length); + int peekBytes = Math.min(peekBufferLength, length); + System.arraycopy(peekBuffer, 0, target, offset, peekBytes); + offset += peekBytes; + length -= peekBytes; + int bytesRead = length != 0 ? dataSource.read(target, offset, length) : 0; if (bytesRead == C.RESULT_END_OF_INPUT) { return C.RESULT_END_OF_INPUT; } + updatePeekBuffer(peekBytes); + bytesRead += peekBytes; position += bytesRead; return bytesRead; } @@ -60,7 +71,10 @@ public final class DefaultExtractorInput implements ExtractorInput { @Override public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException, InterruptedException { - int remaining = length; + int peekBytes = Math.min(peekBufferLength, length); + System.arraycopy(peekBuffer, 0, target, offset, peekBytes); + offset += peekBytes; + int remaining = length - peekBytes; while (remaining > 0) { if (Thread.interrupted()) { throw new InterruptedException(); @@ -75,6 +89,7 @@ public final class DefaultExtractorInput implements ExtractorInput { offset += bytesRead; remaining -= bytesRead; } + updatePeekBuffer(peekBytes); position += length; return true; } @@ -87,7 +102,8 @@ public final class DefaultExtractorInput implements ExtractorInput { @Override public void skipFully(int length) throws IOException, InterruptedException { - int remaining = length; + int peekBytes = Math.min(peekBufferLength, length); + int remaining = length - peekBytes; while (remaining > 0) { if (Thread.interrupted()) { throw new InterruptedException(); @@ -98,9 +114,64 @@ public final class DefaultExtractorInput implements ExtractorInput { } remaining -= bytesRead; } + updatePeekBuffer(peekBytes); position += length; } + @Override + public void peekFully(byte[] target, int offset, int length) + throws IOException, InterruptedException { + ensureSpaceForPeek(length); + int peekBytes = Math.min(peekBufferLength - peekBufferPosition, length); + System.arraycopy(peekBuffer, peekBufferPosition, target, offset, peekBytes); + offset += peekBytes; + int fillBytes = length - peekBytes; + int remaining = fillBytes; + int writePosition = peekBufferLength; + while (remaining > 0) { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + int bytesRead = dataSource.read(peekBuffer, writePosition, remaining); + if (bytesRead == C.RESULT_END_OF_INPUT) { + throw new EOFException(); + } + System.arraycopy(peekBuffer, writePosition, target, offset, bytesRead); + remaining -= bytesRead; + writePosition += bytesRead; + offset += bytesRead; + } + peekBufferPosition += length; + peekBufferLength += fillBytes; + } + + @Override + public void advancePeekPosition(int length) throws IOException, InterruptedException { + ensureSpaceForPeek(length); + int peekBytes = Math.min(peekBufferLength - peekBufferPosition, length); + int fillBytes = length - peekBytes; + int remaining = fillBytes; + int writePosition = peekBufferLength; + while (remaining > 0) { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + int bytesRead = dataSource.read(peekBuffer, writePosition, remaining); + if (bytesRead == C.RESULT_END_OF_INPUT) { + throw new EOFException(); + } + remaining -= bytesRead; + writePosition += bytesRead; + } + peekBufferPosition += length; + peekBufferLength += fillBytes; + } + + @Override + public void resetPeekPosition() { + peekBufferPosition = 0; + } + @Override public long getPosition() { return position; @@ -108,7 +179,29 @@ public final class DefaultExtractorInput implements ExtractorInput { @Override public long getLength() { - return length; + return streamLength; + } + + /** + * Ensures {@code peekBuffer} is large enough to store at least {@code length} bytes from the + * current peek position. + */ + private void ensureSpaceForPeek(int length) { + int requiredLength = peekBufferPosition + length; + if (requiredLength > peekBuffer.length) { + peekBuffer = Arrays.copyOf(peekBuffer, Math.max(peekBuffer.length * 2, requiredLength)); + } + } + + /** + * Updates the peek buffer's length, position and contents after consuming data. + * + * @param bytesConsumed The number of bytes consumed from the peek buffer. + */ + private void updatePeekBuffer(int bytesConsumed) { + peekBufferLength -= bytesConsumed; + peekBufferPosition = 0; + System.arraycopy(peekBuffer, bytesConsumed, peekBuffer, 0, peekBufferLength); } } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/Extractor.java index 9b803a9e34..301bf2f659 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/Extractor.java @@ -49,6 +49,15 @@ public interface Extractor { */ void init(ExtractorOutput output); + /** + * Returns whether this extractor can extract samples from the {@link ExtractorInput}, which must + * provide data from the start of the stream. + * + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + boolean sniff(ExtractorInput input) throws IOException, InterruptedException; + /** * Extracts data read from a provided {@link ExtractorInput}. *

diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorInput.java b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorInput.java index 63c09aab3d..bf5788fc8c 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorInput.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorInput.java @@ -93,9 +93,40 @@ public interface ExtractorInput { void skipFully(int length) throws IOException, InterruptedException; /** - * The current position (byte offset) in the stream. + * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index + * {@code offset}. The current read position is left unchanged. + *

+ * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read + * position, so the caller can peek the same data again. Reading also resets the peek position. * - * @return The position (byte offset) in the stream. + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The number of bytes to peek from the input. + * @throws EOFException If the end of input was encountered. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread is interrupted. + */ + void peekFully(byte[] target, int offset, int length) throws IOException, InterruptedException; + + /** + * Advances the peek position by {@code length} bytes. + * + * @param length The number of bytes to peek from the input. + * @throws EOFException If the end of input was encountered. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread is interrupted. + */ + void advancePeekPosition(int length) throws IOException, InterruptedException; + + /** + * Resets the peek position to equal the current read position. + */ + void resetPeekPosition(); + + /** + * The current read position (byte offset) in the stream. + * + * @return The read position (byte offset) in the stream. */ long getPosition(); diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java index a365c73626..72dad1f667 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java @@ -18,6 +18,7 @@ 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; @@ -36,10 +37,35 @@ 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.Arrays; +import java.util.List; /** - * A {@link SampleSource} that extracts sample data using an {@link Extractor} + * A {@link SampleSource} that extracts sample data using an {@link Extractor}. + * + *

If no {@link Extractor} instances are passed to the constructor, the input stream container + * format will be detected automatically from the following supported formats: + * + *

+ * + *

Seeking in AAC and MPEG TS streams is not supported. + * + *

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 class ExtractorSampleSource implements SampleSource, SampleSourceReader, ExtractorOutput, Loader.Callback { @@ -57,7 +83,61 @@ public class ExtractorSampleSource implements SampleSource, SampleSourceReader, private static final int MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA = -1; private static final int NO_RESET_PENDING = -1; - private final Extractor extractor; + /** + * Default extractor classes in priority order. They are referred to indirectly so that it is + * possible to remove unused extractors. + */ + private static final List> DEFAULT_EXTRACTOR_CLASSES; + static { + DEFAULT_EXTRACTOR_CLASSES = new ArrayList<>(); + // Load extractors using reflection so that they can be deleted cleanly. + // Class.forName() 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 sampleQueues; @@ -101,68 +181,81 @@ public class ExtractorSampleSource implements SampleSource, SampleSourceReader, /** * @param uri The {@link Uri} of the media stream. * @param dataSource A data source to read the media stream. - * @param extractor An {@link Extractor} to extract 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, Extractor extractor, - int requestedBufferSize) { - this(uri, dataSource, extractor, new DefaultAllocator(64 * 1024), requestedBufferSize); + 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 extractor An {@link Extractor} to extract 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, Extractor extractor, - Allocator allocator, int requestedBufferSize) { - this(uri, dataSource, extractor, allocator, requestedBufferSize, - MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA); + 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 extractor An {@link Extractor} to extract 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, Extractor extractor, - int requestedBufferSize, int minLoadableRetryCount) { - this(uri, dataSource, extractor, new DefaultAllocator(64 * 1024), requestedBufferSize, - minLoadableRetryCount); + 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 extractor An {@link Extractor} to extract 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, Extractor extractor, - Allocator allocator, int requestedBufferSize, int minLoadableRetryCount) { + public ExtractorSampleSource(Uri uri, DataSource dataSource, Allocator allocator, + int requestedBufferSize, int minLoadableRetryCount, Extractor... extractors) { this.uri = uri; this.dataSource = dataSource; - this.extractor = extractor; 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 (ReflectiveOperationException e) { + throw new IllegalStateException("Unexpected error creating default extractor", e); + } + } + } + extractorHolder = new ExtractorHolder(extractors, this); sampleQueues = new SparseArray<>(); pendingResetPositionUs = NO_RESET_PENDING; frameAccurateSeeking = true; - extractor.init(this); } @Override @@ -508,11 +601,12 @@ public class ExtractorSampleSource implements SampleSource, SampleSourceReader, } private ExtractingLoadable createLoadableFromStart() { - return new ExtractingLoadable(uri, dataSource, extractor, allocator, requestedBufferSize, 0); + return new ExtractingLoadable(uri, dataSource, extractorHolder, allocator, requestedBufferSize, + 0); } private ExtractingLoadable createLoadableFromPositionUs(long positionUs) { - return new ExtractingLoadable(uri, dataSource, extractor, allocator, requestedBufferSize, + return new ExtractingLoadable(uri, dataSource, extractorHolder, allocator, requestedBufferSize, seekMap.getPosition(positionUs)); } @@ -575,7 +669,7 @@ public class ExtractorSampleSource implements SampleSource, SampleSourceReader, private final Uri uri; private final DataSource dataSource; - private final Extractor extractor; + private final ExtractorHolder extractorHolder; private final Allocator allocator; private final int requestedBufferSize; private final PositionHolder positionHolder; @@ -584,11 +678,11 @@ public class ExtractorSampleSource implements SampleSource, SampleSourceReader, private boolean pendingExtractorSeek; - public ExtractingLoadable(Uri uri, DataSource dataSource, Extractor extractor, + 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.extractor = Assertions.checkNotNull(extractor); + this.extractorHolder = Assertions.checkNotNull(extractorHolder); this.allocator = Assertions.checkNotNull(allocator); this.requestedBufferSize = requestedBufferSize; positionHolder = new PositionHolder(); @@ -608,10 +702,6 @@ public class ExtractorSampleSource implements SampleSource, SampleSourceReader, @Override public void load() throws IOException, InterruptedException { - if (pendingExtractorSeek) { - extractor.seek(); - pendingExtractorSeek = false; - } int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { ExtractorInput input = null; @@ -622,6 +712,11 @@ public class ExtractorSampleSource implements SampleSource, SampleSourceReader, 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); @@ -640,4 +735,69 @@ public class ExtractorSampleSource implements SampleSource, SampleSourceReader, } + /** + * 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; + } + + } + + /** + * Thrown if the input format could not recognized by {@link Extractor#sniff(ExtractorInput)}. + */ + private static final class UnrecognizedInputFormatException extends ParserException { + + public UnrecognizedInputFormatException(Extractor[] extractors) { + super("None of the extractors " + Arrays.toString(extractors) + " could read the stream."); + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java index 5411f496b1..deee4d3a33 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java @@ -37,7 +37,9 @@ import java.io.IOException; public final class Mp3Extractor implements Extractor { /** The maximum number of bytes to search when synchronizing, before giving up. */ - private static final int MAX_BYTES_TO_SEARCH = 128 * 1024; + private static final int MAX_SYNC_BYTES = 128 * 1024; + /** The maximum number of bytes to read when sniffing, excluding the header, before giving up. */ + private static final int MAX_SNIFF_BYTES = 4 * 1024; /** Mask that includes the audio header values that must match between frames. */ private static final int HEADER_MASK = 0xFFFE0C00; @@ -68,6 +70,61 @@ public final class Mp3Extractor implements Extractor { synchronizedHeader = new MpegAudioHeader(); } + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(4); + int startPosition = 0; + input.peekFully(scratch.data, 0, 3); + if (scratch.readUnsignedInt24() == ID3_TAG) { + input.advancePeekPosition(3); + input.peekFully(scratch.data, 0, 4); + int headerLength = ((scratch.data[0] & 0x7F) << 21) | ((scratch.data[1] & 0x7F) << 14) + | ((scratch.data[2] & 0x7F) << 7) | (scratch.data[3] & 0x7F); + input.advancePeekPosition(headerLength); + startPosition = 3 + 3 + 4 + headerLength; + } else { + input.resetPeekPosition(); + } + + // Try to find four consecutive valid MPEG audio frames. + int headerPosition = startPosition; + int validFrameCount = 0; + int candidateSynchronizedHeaderData = 0; + while (true) { + if (headerPosition - startPosition >= MAX_SNIFF_BYTES) { + return false; + } + + input.peekFully(scratch.data, 0, 4); + scratch.setPosition(0); + int headerData = scratch.readInt(); + int frameSize; + if ((candidateSynchronizedHeaderData != 0 + && (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK)) + || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == -1) { + validFrameCount = 0; + candidateSynchronizedHeaderData = 0; + + // Try reading a header starting at the next byte. + input.resetPeekPosition(); + input.advancePeekPosition(++headerPosition); + continue; + } + + if (validFrameCount == 0) { + candidateSynchronizedHeaderData = headerData; + } + + // The header was valid and matching (if appropriate). Check another or end synchronization. + if (++validFrameCount == 4) { + return true; + } + + // Look for more headers. + input.advancePeekPosition(frameSize - 4); + } + } + @Override public void init(ExtractorOutput extractorOutput) { this.extractorOutput = extractorOutput; @@ -167,6 +224,7 @@ public final class Mp3Extractor implements Extractor { } private long synchronize(ExtractorInput extractorInput) throws IOException, InterruptedException { + // TODO: Use peekFully instead of a buffering input, and deduplicate with sniff(). if (extractorInput.getPosition() == 0) { // Before preparation completes, retrying loads from the start, so clear any buffered data. inputBuffer.reset(); @@ -201,7 +259,7 @@ public final class Mp3Extractor implements Extractor { int validFrameCount = 0; int candidateSynchronizedHeaderData = 0; while (true) { - if (headerPosition - startPosition >= MAX_BYTES_TO_SEARCH) { + if (headerPosition - startPosition >= MAX_SYNC_BYTES) { throw new ParserException("Searched too many bytes while resynchronizing."); } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java index 6469462394..31b8bc691a 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java @@ -114,6 +114,11 @@ public final class FragmentedMp4Extractor implements Extractor { parserState = STATE_READING_ATOM_HEADER; } + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return Sniffer.sniffFragmented(input); + } + /** * Sideloads track information into the extractor. *

diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java index 9ea68fa2ec..8a30a9bcac 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java @@ -77,6 +77,11 @@ public final class Mp4Extractor implements Extractor, SeekMap { parserState = STATE_READING_ATOM_HEADER; } + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return Sniffer.sniffUnfragmented(input); + } + @Override public void init(ExtractorOutput output) { extractorOutput = output; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Sniffer.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Sniffer.java new file mode 100644 index 0000000000..bb65ab68e1 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Sniffer.java @@ -0,0 +1,160 @@ +/* + * 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.mp4; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.extractor.ExtractorInput; +import com.google.android.exoplayer.util.ParsableByteArray; +import com.google.android.exoplayer.util.Util; + +import java.io.IOException; + +/** + * Provides methods that peek data from an {@link ExtractorInput} and return whether the input + * appears to be in MP4 format. + */ +/* package */ final class Sniffer { + + private static final int[] COMPATIBLE_BRANDS = new int[] { + Util.getIntegerCodeForString("isom"), + Util.getIntegerCodeForString("iso2"), + Util.getIntegerCodeForString("avc1"), + Util.getIntegerCodeForString("hvc1"), + Util.getIntegerCodeForString("hev1"), + Util.getIntegerCodeForString("mp41"), + Util.getIntegerCodeForString("mp42"), + Util.getIntegerCodeForString("3g2a"), + Util.getIntegerCodeForString("3g2b"), + Util.getIntegerCodeForString("3gr6"), + Util.getIntegerCodeForString("3gs6"), + Util.getIntegerCodeForString("3ge6"), + Util.getIntegerCodeForString("3gg6"), + Util.getIntegerCodeForString("M4V "), + Util.getIntegerCodeForString("M4A "), + Util.getIntegerCodeForString("f4v "), + Util.getIntegerCodeForString("kddi"), + Util.getIntegerCodeForString("M4VP"), + Util.getIntegerCodeForString("qt "), // Apple QuickTime + Util.getIntegerCodeForString("MSNV"), // Sony PSP + }; + + /** + * Returns whether data peeked from the current position in {@code input} is consistent with the + * input being a fragmented MP4 file. + * + * @param input The extractor input from which to peek data. The peek position will be modified. + * @return True if the input appears to be in the fragmented MP4 format. False otherwise. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + public static boolean sniffFragmented(ExtractorInput input) + throws IOException, InterruptedException { + return sniffInternal(input, 4 * 1024, true); + } + + /** + * Returns whether data peeked from the current position in {@code input} is consistent with the + * input being an unfragmented MP4 file. + * + * @param input The extractor input from which to peek data. The peek position will be modified. + * @return True if the input appears to be in the unfragmented MP4 format. False otherwise. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + public static boolean sniffUnfragmented(ExtractorInput input) + throws IOException, InterruptedException { + return sniffInternal(input, 128, false); + } + + private static boolean sniffInternal(ExtractorInput input, int searchLength, boolean fragmented) + throws IOException, InterruptedException { + long inputLength = input.getLength(); + int bytesToSearch = (int) (inputLength == C.LENGTH_UNBOUNDED || inputLength > searchLength + ? searchLength : inputLength); + + ParsableByteArray buffer = new ParsableByteArray(64); + int bytesSearched = 0; + boolean foundGoodFileType = false; + boolean foundFragment = false; + while (bytesSearched < bytesToSearch) { + // Read an atom header. + int headerSize = Atom.HEADER_SIZE; + input.peekFully(buffer.data, 0, headerSize); + buffer.setPosition(0); + long atomSize = buffer.readUnsignedInt(); + int atomType = buffer.readInt(); + if (atomSize == Atom.LONG_SIZE_PREFIX) { + input.peekFully(buffer.data, headerSize, Atom.LONG_HEADER_SIZE - headerSize); + headerSize = Atom.LONG_HEADER_SIZE; + atomSize = buffer.readLong(); + } + // Check the atom size is large enough to include its header. + if (atomSize <= headerSize || atomSize > Integer.MAX_VALUE) { + return false; + } + // Stop searching if reading this atom would exceed the search limit. + if (bytesSearched + atomSize > bytesToSearch) { + break; + } + int atomDataSize = (int) atomSize - headerSize; + if (atomType == Atom.TYPE_ftyp) { + if (atomDataSize < 8) { + return false; + } + int compatibleBrandsCount = (atomDataSize - 8) / 4; + input.peekFully(buffer.data, 0, 4 * (compatibleBrandsCount + 2)); + for (int i = 0; i < compatibleBrandsCount + 2; i++) { + if (i == 1) { + // This index refers to the minorVersion, not a brand, so skip it. + continue; + } + if (isCompatibleBrand(buffer.readInt())) { + foundGoodFileType = true; + break; + } + } + } else if (atomType == Atom.TYPE_moof) { + foundFragment = true; + break; + } else { + input.advancePeekPosition(atomDataSize); + } + bytesSearched += atomSize; + } + return foundGoodFileType && fragmented == foundFragment; + } + + /** + * Returns whether {@code brand} is an ftyp atom brand that is compatible with the MP4 extractors. + */ + private static boolean isCompatibleBrand(int brand) { + // Accept all brands starting '3gp'. + if (brand >>> 8 == Util.getIntegerCodeForString("3gp")) { + return true; + } + for (int compatibleBrand : COMPATIBLE_BRANDS) { + if (compatibleBrand == brand) { + return true; + } + } + return false; + } + + private Sniffer() { + // Prevent instantiation. + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java index c1e8c55118..315cc2f497 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer.extractor.ExtractorOutput; import com.google.android.exoplayer.extractor.PositionHolder; import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.util.ParsableByteArray; +import com.google.android.exoplayer.util.Util; import java.io.IOException; @@ -49,6 +50,24 @@ public class AdtsExtractor implements Extractor { firstPacket = true; } + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(10); + input.peekFully(scratch.data, 0, 10); + int value = scratch.readUnsignedInt24(); + if (value != Util.getIntegerCodeForString("ID3")) { + value = value >> 8; + } else { + int length = (scratch.data[6] & 0x7F) << 21 | ((scratch.data[7] & 0x7F) << 14) + | ((scratch.data[8] & 0x7F) << 7) | (scratch.data[9] & 0x7F); + input.advancePeekPosition(length); + input.peekFully(scratch.data, 0, 2); + scratch.setPosition(0); + value = scratch.readUnsignedShort(); + } + return (value & 0xFFF6) == 0xFFF0; + } + @Override public void init(ExtractorOutput output) { adtsReader = new AdtsReader(output.track(0)); diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java index 8f3ccaec54..2d196d5cb9 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java @@ -95,6 +95,19 @@ public final class TsExtractor implements Extractor { // Extractor implementation. + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + byte[] scratch = new byte[1]; + for (int i = 0; i < 5; i++) { + input.peekFully(scratch, 0, 1); + if ((scratch[0] & 0xFF) != 0x47) { + return false; + } + input.advancePeekPosition(TS_PACKET_SIZE - 1); + } + return true; + } + @Override public void init(ExtractorOutput output) { this.output = output; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/webm/Sniffer.java b/library/src/main/java/com/google/android/exoplayer/extractor/webm/Sniffer.java new file mode 100644 index 0000000000..4edc79a553 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/webm/Sniffer.java @@ -0,0 +1,113 @@ +/* + * 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.webm; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.extractor.Extractor; +import com.google.android.exoplayer.extractor.ExtractorInput; +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.io.IOException; + +/** + * Utility class that peeks from the input stream in order to determine whether it appears to be + * compatible input for this extractor. + */ +/* package */ final class Sniffer { + + /** + * The number of bytes to search for a valid header in {@link #sniff(ExtractorInput)}. + */ + private static final int SEARCH_LENGTH = 1024; + private static final int ID_EBML = 0x1A45DFA3; + + private final ParsableByteArray scratch; + private int peekLength; + + public Sniffer() { + scratch = new ParsableByteArray(8); + } + + /** + * @see Extractor#sniff + */ + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + long inputLength = input.getLength(); + int bytesToSearch = (int) (inputLength == C.LENGTH_UNBOUNDED || inputLength > SEARCH_LENGTH + ? SEARCH_LENGTH : inputLength); + // Find four bytes equal to ID_EBML near the start of the input. + input.peekFully(scratch.data, 0, 4); + long tag = scratch.readUnsignedInt(); + peekLength = 4; + while (tag != ID_EBML) { + if (++peekLength == bytesToSearch) { + return false; + } + input.peekFully(scratch.data, 0, 1); + tag = (tag << 8) & 0xFFFFFF00; + tag |= scratch.data[0] & 0xFF; + } + + // Read the size of the EBML header and make sure it is within the stream. + long headerSize = readUint(input); + long headerStart = peekLength; + if (headerSize == Long.MIN_VALUE + || (inputLength != C.LENGTH_UNBOUNDED && headerStart + headerSize >= inputLength)) { + return false; + } + + // Read the payload elements in the EBML header. + while (peekLength < headerStart + headerSize) { + long id = readUint(input); + if (id == Long.MIN_VALUE) { + return false; + } + long size = readUint(input); + if (size <= 0 || size > Integer.MAX_VALUE) { + return false; + } + input.advancePeekPosition((int) size); + peekLength += size; + } + return peekLength == headerStart + headerSize; + } + + /** + * Peeks a variable-length unsigned EBML integer from the input. + */ + private long readUint(ExtractorInput input) throws IOException, InterruptedException { + input.peekFully(scratch.data, 0, 1); + int value = scratch.data[0] & 0xFF; + if (value == 0) { + return Long.MIN_VALUE; + } + int mask = 0x80; + int length = 0; + while ((value & mask) == 0) { + mask >>= 1; + length++; + } + value &= ~mask; + input.peekFully(scratch.data, 1, length); + for (int i = 0; i < length; i++) { + value <<= 8; + value += scratch.data[i + 1] & 0xFF; + } + peekLength += length + 1; + return value; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java index 16b045aa9f..c41b97403d 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java @@ -211,6 +211,11 @@ public final class WebmExtractor implements Extractor { sampleStrippedBytes = new ParsableByteArray(); } + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return new Sniffer().sniff(input); + } + @Override public void init(ExtractorOutput output) { extractorOutput = output;