diff --git a/library/src/main/java/com/google/android/exoplayer/SampleSource.java b/library/src/main/java/com/google/android/exoplayer/SampleSource.java
index 8e2afc32d6..9c5d6aa303 100644
--- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java
+++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java
@@ -54,8 +54,8 @@ public interface SampleSource {
* Prepares the source.
*
* Preparation may require reading from the data source (e.g. to determine the available tracks
- * and formats). If insufficient data is available then the call will return rather than block.
- * The method can be called repeatedly until the return value indicates success.
+ * and formats). If insufficient data is available then the call will return {@code false} rather
+ * than block. The method can be called repeatedly until the return value indicates success.
*
* @return True if the source was prepared successfully, false otherwise.
* @throws IOException If an error occurred preparing the source.
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java
index b874f09597..33a4d08a7d 100644
--- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java
@@ -160,6 +160,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp;
+ private MediaFormat downstreamMediaFormat;
private volatile Format downstreamFormat;
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
@@ -221,6 +222,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
chunkSource.enable();
loadControl.register(this, bufferSizeContribution);
downstreamFormat = null;
+ downstreamMediaFormat = null;
downstreamPositionUs = timeUs;
lastSeekPositionUs = timeUs;
restartFrom(timeUs);
@@ -288,21 +290,30 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
return readData(track, playbackPositionUs, formatHolder, sampleHolder, false);
} else if (mediaChunk.isLastChunk()) {
return END_OF_STREAM;
- } else {
- IOException chunkSourceException = chunkSource.getError();
- if (chunkSourceException != null) {
- throw chunkSourceException;
- }
- return NOTHING_READ;
}
- } else if (downstreamFormat == null || !downstreamFormat.id.equals(mediaChunk.format.id)) {
+ IOException chunkSourceException = chunkSource.getError();
+ if (chunkSourceException != null) {
+ throw chunkSourceException;
+ }
+ return NOTHING_READ;
+ }
+
+ if (downstreamFormat == null || !downstreamFormat.equals(mediaChunk.format)) {
notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger,
mediaChunk.startTimeUs);
- MediaFormat format = mediaChunk.getMediaFormat();
- chunkSource.getMaxVideoDimensions(format);
- formatHolder.format = format;
- formatHolder.drmInitData = mediaChunk.getPsshInfo();
downstreamFormat = mediaChunk.format;
+ }
+
+ if (!mediaChunk.prepare()) {
+ return NOTHING_READ;
+ }
+
+ MediaFormat mediaFormat = mediaChunk.getMediaFormat();
+ if (downstreamMediaFormat == null || !downstreamMediaFormat.equals(mediaFormat)) {
+ chunkSource.getMaxVideoDimensions(mediaFormat);
+ formatHolder.format = mediaFormat;
+ formatHolder.drmInitData = mediaChunk.getPsshInfo();
+ downstreamMediaFormat = mediaFormat;
return FORMAT_READ;
}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Format.java b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java
index 4aabe601dc..b33fb3aeea 100644
--- a/library/src/main/java/com/google/android/exoplayer/chunk/Format.java
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer.chunk;
+import com.google.android.exoplayer.util.Assertions;
+
import java.util.Comparator;
/**
@@ -97,7 +99,7 @@ public class Format {
*/
public Format(String id, String mimeType, int width, int height, int numChannels,
int audioSamplingRate, int bandwidth) {
- this.id = id;
+ this.id = Assertions.checkNotNull(id);
this.mimeType = mimeType;
this.width = width;
this.height = height;
@@ -106,4 +108,19 @@ public class Format {
this.bandwidth = bandwidth;
}
+ /**
+ * Implements equality based on {@link #id} only.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ Format other = (Format) obj;
+ return other.id.equals(id);
+ }
+
}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java
index fd7c3a59b6..51ebac65c2 100644
--- a/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java
@@ -87,8 +87,22 @@ public abstract class MediaChunk extends Chunk {
*/
public abstract boolean seekTo(long positionUs, boolean allowNoop);
+ /**
+ * Prepares the chunk for reading. Does nothing if the chunk is already prepared.
+ *
+ * Preparation may require consuming some of the chunk. If the data is not yet available then
+ * this method will return {@code false} rather than block. The method can be called repeatedly
+ * until the return value indicates success.
+ *
+ * @return True if the chunk was prepared. False otherwise.
+ * @throws ParserException If an error occurs parsing the media data.
+ */
+ public abstract boolean prepare() throws ParserException;
+
/**
* Reads the next media sample from the chunk.
+ *
+ * Should only be called after the chunk has been successfully prepared.
*
* @param holder A holder to store the read sample.
* @return True if a sample was read. False if more data is still required.
@@ -99,6 +113,8 @@ public abstract class MediaChunk extends Chunk {
/**
* Returns the media format of the samples contained within this chunk.
+ *
+ * Should only be called after the chunk has been successfully prepared.
*
* @return The sample media format.
*/
@@ -106,6 +122,8 @@ public abstract class MediaChunk extends Chunk {
/**
* Returns the pssh information associated with the chunk.
+ *
+ * Should only be called after the chunk has been successfully prepared.
*
* @return The pssh information.
*/
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java
index 4bd0076a6d..8aaee879e4 100644
--- a/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java
@@ -33,24 +33,34 @@ import java.util.UUID;
public final class Mp4MediaChunk extends MediaChunk {
private final FragmentedMp4Extractor extractor;
+ private final boolean maybeSelfContained;
private final long sampleOffsetUs;
+ private boolean prepared;
+ private MediaFormat mediaFormat;
+ private Map psshInfo;
+
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param format The format of the stream to which this chunk belongs.
- * @param extractor The extractor that will be used to extract the samples.
* @param trigger The reason for this chunk being selected.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
- * @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor.
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
+ * @param extractor The extractor that will be used to extract the samples.
+ * @param maybeSelfContained Set to true if this chunk might be self contained, meaning it might
+ * contain a moov atom defining the media format of the chunk. This parameter can always be
+ * safely set to true. Setting to false where the chunk is known to not be self contained may
+ * improve startup latency.
+ * @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor.
*/
public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
- int trigger, FragmentedMp4Extractor extractor, long startTimeUs, long endTimeUs,
- long sampleOffsetUs, int nextChunkIndex) {
+ int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex,
+ FragmentedMp4Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) {
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
this.extractor = extractor;
+ this.maybeSelfContained = maybeSelfContained;
this.sampleOffsetUs = sampleOffsetUs;
}
@@ -70,6 +80,29 @@ public final class Mp4MediaChunk extends MediaChunk {
return isDiscontinuous;
}
+ @Override
+ public boolean prepare() throws ParserException {
+ if (!prepared) {
+ if (maybeSelfContained) {
+ // Read up to the first sample. Once we're there, we know that the extractor must have
+ // parsed a moov atom if the chunk contains one.
+ NonBlockingInputStream inputStream = getNonBlockingInputStream();
+ Assertions.checkState(inputStream != null);
+ int result = extractor.read(inputStream, null);
+ prepared = (result & FragmentedMp4Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
+ } else {
+ // We know there isn't a moov atom. The extractor must have parsed one from a separate
+ // initialization chunk.
+ prepared = true;
+ }
+ if (prepared) {
+ mediaFormat = Assertions.checkNotNull(extractor.getFormat());
+ psshInfo = extractor.getPsshInfo();
+ }
+ }
+ return prepared;
+ }
+
@Override
public boolean read(SampleHolder holder) throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
@@ -84,12 +117,12 @@ public final class Mp4MediaChunk extends MediaChunk {
@Override
public MediaFormat getMediaFormat() {
- return extractor.getFormat();
+ return mediaFormat;
}
@Override
public Map getPsshInfo() {
- return extractor.getPsshInfo();
+ return psshInfo;
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java
index ef7e1436a0..6fa2f08962 100644
--- a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java
@@ -77,6 +77,11 @@ public class SingleSampleMediaChunk extends MediaChunk {
this.headerData = headerData;
}
+ @Override
+ public boolean prepare() {
+ return true;
+ }
+
@Override
public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java
index 23dfa2bf0a..f7ca26244a 100644
--- a/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java
@@ -64,6 +64,11 @@ public final class WebmMediaChunk extends MediaChunk {
return isDiscontinuous;
}
+ @Override
+ public boolean prepare() {
+ return true;
+ }
+
@Override
public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java
index e975660497..4bf07c1b3a 100644
--- a/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java
+++ b/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java
@@ -231,8 +231,8 @@ public class DashMp4ChunkSource implements ChunkSource {
DataSpec dataSpec = new DataSpec(representation.uri, offset, size,
representation.getCacheKey());
- return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
- startTimeUs, endTimeUs, 0, nextIndex);
+ return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs,
+ endTimeUs, nextIndex, extractor, false, 0);
}
private static class InitializationMp4Loadable extends Chunk {
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java
index 9c44999e80..16e1788943 100644
--- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java
+++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java
@@ -83,9 +83,13 @@ public final class FragmentedMp4Extractor {
* A sidx atom was read. The parsed data can be read using {@link #getSegmentIndex()}.
*/
public static final int RESULT_READ_SIDX = 32;
+ /**
+ * The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
+ */
+ public static final int RESULT_NEED_SAMPLE_HOLDER = 64;
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
- | RESULT_READ_SAMPLE_FULL;
+ | RESULT_READ_SAMPLE_FULL | RESULT_NEED_SAMPLE_HOLDER;
private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
@@ -272,7 +276,8 @@ public final class FragmentedMp4Extractor {
* in subsequent calls until the whole sample has been read.
*
* @param inputStream The input stream from which data should be read.
- * @param out A {@link SampleHolder} into which the sample should be read.
+ * @param out A {@link SampleHolder} into which the next sample should be read. If null then
+ * {@link #RESULT_NEED_SAMPLE_HOLDER} will be returned once a sample has been reached.
* @return One or more of the {@code RESULT_*} flags defined in this class.
* @throws ParserException If an error occurs parsing the media data.
*/
@@ -1142,6 +1147,9 @@ public final class FragmentedMp4Extractor {
@SuppressLint("InlinedApi")
private int readSample(NonBlockingInputStream inputStream, SampleHolder out) {
+ if (out == null) {
+ return RESULT_NEED_SAMPLE_HOLDER;
+ }
int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
ByteBuffer outputData = out.data;
if (parserState == STATE_READING_SAMPLE_START) {
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java
index b0b0936fe1..351eff32d9 100644
--- a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java
+++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java
@@ -36,7 +36,7 @@ import java.util.concurrent.TimeUnit;
* More info about WebM is here.
*/
@TargetApi(16)
-public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandler {
+public final class DefaultWebmExtractor implements WebmExtractor {
private static final String DOC_TYPE_WEBM = "webm";
private static final String CODEC_ID_VP9 = "V_VP9";
@@ -104,7 +104,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
/* package */ DefaultWebmExtractor(EbmlReader reader) {
this.reader = reader;
- this.reader.setEventHandler(this);
+ this.reader.setEventHandler(new InnerEbmlEventHandler());
this.cueTimesUs = new LongArray();
this.cueClusterPositions = new LongArray();
}
@@ -150,8 +150,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
return format;
}
- @Override
- public int getElementType(int id) {
+ /* package */ int getElementType(int id) {
switch (id) {
case ID_EBML:
case ID_SEGMENT:
@@ -185,8 +184,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
}
}
- @Override
- public boolean onMasterElementStart(
+ /* package */ boolean onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
switch (id) {
case ID_SEGMENT:
@@ -205,8 +203,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
return true;
}
- @Override
- public boolean onMasterElementEnd(int id) {
+ /* package */ boolean onMasterElementEnd(int id) {
if (id == ID_CUES) {
finishPreparing();
return false;
@@ -214,8 +211,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
return true;
}
- @Override
- public boolean onIntegerElement(int id, long value) {
+ /* package */ boolean onIntegerElement(int id, long value) {
switch (id) {
case ID_EBML_READ_VERSION:
// Validate that EBMLReadVersion is supported. This extractor only supports v1.
@@ -253,16 +249,14 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
return true;
}
- @Override
- public boolean onFloatElement(int id, double value) {
+ /* package */ boolean onFloatElement(int id, double value) {
if (id == ID_DURATION) {
durationUs = scaleTimecodeToUs((long) value);
}
return true;
}
- @Override
- public boolean onStringElement(int id, String value) {
+ /* package */ boolean onStringElement(int id, String value) {
switch (id) {
case ID_DOC_TYPE:
// Validate that DocType is supported. This extractor only supports "webm".
@@ -282,8 +276,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
return true;
}
- @Override
- public boolean onBinaryElement(
+ /* package */ boolean onBinaryElement(
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
NonBlockingInputStream inputStream) {
if (id == ID_SIMPLE_BLOCK) {
@@ -383,4 +376,52 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
prepared = true;
}
+ /**
+ * Passes events through to {@link DefaultWebmExtractor} as
+ * callbacks from {@link EbmlReader} are received.
+ */
+ private final class InnerEbmlEventHandler implements EbmlEventHandler {
+
+ @Override
+ public int getElementType(int id) {
+ return DefaultWebmExtractor.this.getElementType(id);
+ }
+
+ @Override
+ public boolean onMasterElementStart(
+ int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
+ return DefaultWebmExtractor.this.onMasterElementStart(
+ id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
+ }
+
+ @Override
+ public boolean onMasterElementEnd(int id) {
+ return DefaultWebmExtractor.this.onMasterElementEnd(id);
+ }
+
+ @Override
+ public boolean onIntegerElement(int id, long value) {
+ return DefaultWebmExtractor.this.onIntegerElement(id, value);
+ }
+
+ @Override
+ public boolean onFloatElement(int id, double value) {
+ return DefaultWebmExtractor.this.onFloatElement(id, value);
+ }
+
+ @Override
+ public boolean onStringElement(int id, String value) {
+ return DefaultWebmExtractor.this.onStringElement(id, value);
+ }
+
+ @Override
+ public boolean onBinaryElement(
+ int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
+ NonBlockingInputStream inputStream) {
+ return DefaultWebmExtractor.this.onBinaryElement(
+ id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes, inputStream);
+ }
+
+ }
+
}
diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java
index 6f2a2490a2..e8ed35d239 100644
--- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java
+++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java
@@ -235,8 +235,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey);
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.
// To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs.
- return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, extractor,
- chunkStartTimeUs, nextStartTimeUs, -chunkStartTimeUs, nextChunkIndex);
+ return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, chunkStartTimeUs,
+ nextStartTimeUs, nextChunkIndex, extractor, false, -chunkStartTimeUs);
}
private static byte[] getKeyId(byte[] initData) {