From 4366afc273d058928b947d3e6795484eb6d7bc19 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 15 Jul 2014 12:47:08 +0100 Subject: [PATCH] Support self-contained media chunks. - Support parsing of moov atoms contained within each chunk. - Also do a small cleanup to WebM parser. --- .../android/exoplayer/SampleSource.java | 4 +- .../exoplayer/chunk/ChunkSampleSource.java | 33 ++++++--- .../android/exoplayer/chunk/Format.java | 19 ++++- .../android/exoplayer/chunk/MediaChunk.java | 18 +++++ .../exoplayer/chunk/Mp4MediaChunk.java | 45 ++++++++++-- .../chunk/SingleSampleMediaChunk.java | 5 ++ .../exoplayer/chunk/WebmMediaChunk.java | 5 ++ .../exoplayer/dash/DashMp4ChunkSource.java | 4 +- .../parser/mp4/FragmentedMp4Extractor.java | 12 ++- .../parser/webm/DefaultWebmExtractor.java | 73 +++++++++++++++---- .../SmoothStreamingChunkSource.java | 4 +- 11 files changed, 180 insertions(+), 42 deletions(-) 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) {