mirror of
https://github.com/samsonjs/media.git
synced 2026-04-03 10:55:48 +00:00
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:
parent
87daa912d7
commit
85e0bca33d
17 changed files with 732 additions and 103 deletions
|
|
@ -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,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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue