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
This commit is contained in:
Oliver Woodman 2015-07-21 17:39:38 +01:00
parent 87daa912d7
commit 85e0bca33d
17 changed files with 732 additions and 103 deletions

View file

@ -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);
}

View file

@ -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() {}

View file

@ -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);

View file

@ -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) {

View file

@ -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

View file

@ -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);
}
}

View file

@ -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}.
* <p>

View file

@ -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.
*
* @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();

View file

@ -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}.
*
* <p>If no {@link Extractor} instances are passed to the constructor, the input stream container
* format will be detected automatically from the following supported formats:
*
* <ul>
* <li>Fragmented MP4
* ({@link com.google.android.exoplayer.extractor.mp4.FragmentedMp4Extractor})</li>
* <li>Unfragmented MP4, including M4A
* ({@link com.google.android.exoplayer.extractor.mp4.Mp4Extractor})</li>
* <li>Matroska, including WebM
* ({@link com.google.android.exoplayer.extractor.webm.WebmExtractor})</li>
* <li>MP3 ({@link com.google.android.exoplayer.extractor.mp3.Mp3Extractor})</li>
* <li>AAC ({@link com.google.android.exoplayer.extractor.ts.AdtsExtractor})</li>
* <li>MPEG TS ({@link com.google.android.exoplayer.extractor.ts.TsExtractor}</li>
* </ul>
*
* <p>Seeking in AAC and MPEG TS streams is not supported.
*
* <p>To override the default extractors, pass one or more {@link Extractor} instances to the
* constructor. When reading a new stream, the first {@link Extractor} that returns {@code true}
* from {@link Extractor#sniff(ExtractorInput)} will be used.
*/
public 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<Class<? extends Extractor>> DEFAULT_EXTRACTOR_CLASSES;
static {
DEFAULT_EXTRACTOR_CLASSES = new ArrayList<>();
// Load extractors using reflection so that they can be deleted cleanly.
// Class.forName(<class name>) appears for each extractor so that automated tools like proguard
// can detect the use of reflection (see http://proguard.sourceforge.net/FAQ.html#forname).
try {
DEFAULT_EXTRACTOR_CLASSES.add(
Class.forName("com.google.android.exoplayer.extractor.webm.WebmExtractor")
.asSubclass(Extractor.class));
} catch (ClassNotFoundException e) {
// Extractor not found.
}
try {
DEFAULT_EXTRACTOR_CLASSES.add(
Class.forName("com.google.android.exoplayer.extractor.mp4.FragmentedMp4Extractor")
.asSubclass(Extractor.class));
} catch (ClassNotFoundException e) {
// Extractor not found.
}
try {
DEFAULT_EXTRACTOR_CLASSES.add(
Class.forName("com.google.android.exoplayer.extractor.mp4.Mp4Extractor")
.asSubclass(Extractor.class));
} catch (ClassNotFoundException e) {
// Extractor not found.
}
try {
DEFAULT_EXTRACTOR_CLASSES.add(
Class.forName("com.google.android.exoplayer.extractor.mp3.Mp3Extractor")
.asSubclass(Extractor.class));
} catch (ClassNotFoundException e) {
// Extractor not found.
}
try {
DEFAULT_EXTRACTOR_CLASSES.add(
Class.forName("com.google.android.exoplayer.extractor.ts.AdtsExtractor")
.asSubclass(Extractor.class));
} catch (ClassNotFoundException e) {
// Extractor not found.
}
try {
DEFAULT_EXTRACTOR_CLASSES.add(
Class.forName("com.google.android.exoplayer.extractor.ts.TsExtractor")
.asSubclass(Extractor.class));
} catch (ClassNotFoundException e) {
// Extractor not found.
}
}
private final ExtractorHolder extractorHolder;
private final Allocator allocator;
private final int requestedBufferSize;
private final SparseArray<InternalTrackOutput> sampleQueues;
@ -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.");
}
}
}

View file

@ -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.");
}

View file

@ -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>

View file

@ -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;

View file

@ -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.
}
}

View file

@ -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));

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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;