Commit 85e0bca3 by Oliver Woodman

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
parent 87daa912
......@@ -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);
}
......
......@@ -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() {}
......
......@@ -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);
......
......@@ -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) {
......
......@@ -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;
* <p>
* 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:
* <ul>
* <li>Playing a container format for which an ExoPlayer extractor does not yet exist (e.g. ogg).
* </li>
* <li>Playing media whose container format is unknown and so needs to be inferred automatically.
* </li>
* </ul>
* 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.
* <p>
* 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
......
......@@ -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,17 +114,94 @@ 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;
}
@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);
}
}
......@@ -50,6 +50,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}.
* <p>
* A single call to this method will block until some progress has been made, but will not block
......
......@@ -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.
* <p>
* 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.
*
* @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 position (byte offset) in the stream.
* @return The read position (byte offset) in the stream.
*/
long getPosition();
......
......@@ -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;
......@@ -69,6 +71,61 @@ public final class Mp3Extractor implements Extractor {
}
@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;
trackOutput = extractorOutput.track(0);
......@@ -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.");
}
......
......@@ -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.
* <p>
......
......@@ -78,6 +78,11 @@ public final class Mp4Extractor implements Extractor, SeekMap {
}
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
return Sniffer.sniffUnfragmented(input);
}
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
}
......
/*
* 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.
}
}
......@@ -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;
......@@ -50,6 +51,24 @@ public class AdtsExtractor implements Extractor {
}
@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));
output.endTracks();
......
......@@ -96,6 +96,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;
output.seekMap(SeekMap.UNSEEKABLE);
......
/*
* 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;
}
}
......@@ -212,6 +212,11 @@ public final class WebmExtractor implements Extractor {
}
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
return new Sniffer().sniff(input);
}
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment