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: - *
- * 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
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;