Support self-contained media chunks.

- Support parsing of moov atoms contained within each chunk.
- Also do a small cleanup to WebM parser.
This commit is contained in:
Oliver Woodman 2014-07-15 12:47:08 +01:00
parent 16fe6a809e
commit 4366afc273
11 changed files with 180 additions and 42 deletions

View File

@ -54,8 +54,8 @@ public interface SampleSource {
* Prepares the source. * Prepares the source.
* <p> * <p>
* Preparation may require reading from the data source (e.g. to determine the available tracks * 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. * and formats). If insufficient data is available then the call will return {@code false} rather
* The method can be called repeatedly until the return value indicates success. * than block. The method can be called repeatedly until the return value indicates success.
* *
* @return True if the source was prepared successfully, false otherwise. * @return True if the source was prepared successfully, false otherwise.
* @throws IOException If an error occurred preparing the source. * @throws IOException If an error occurred preparing the source.

View File

@ -160,6 +160,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
private int currentLoadableExceptionCount; private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp; private long currentLoadableExceptionTimestamp;
private MediaFormat downstreamMediaFormat;
private volatile Format downstreamFormat; private volatile Format downstreamFormat;
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
@ -221,6 +222,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
chunkSource.enable(); chunkSource.enable();
loadControl.register(this, bufferSizeContribution); loadControl.register(this, bufferSizeContribution);
downstreamFormat = null; downstreamFormat = null;
downstreamMediaFormat = null;
downstreamPositionUs = timeUs; downstreamPositionUs = timeUs;
lastSeekPositionUs = timeUs; lastSeekPositionUs = timeUs;
restartFrom(timeUs); restartFrom(timeUs);
@ -288,21 +290,30 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
return readData(track, playbackPositionUs, formatHolder, sampleHolder, false); return readData(track, playbackPositionUs, formatHolder, sampleHolder, false);
} else if (mediaChunk.isLastChunk()) { } else if (mediaChunk.isLastChunk()) {
return END_OF_STREAM; return END_OF_STREAM;
} else { }
IOException chunkSourceException = chunkSource.getError(); IOException chunkSourceException = chunkSource.getError();
if (chunkSourceException != null) { if (chunkSourceException != null) {
throw chunkSourceException; throw chunkSourceException;
} }
return NOTHING_READ; return NOTHING_READ;
} }
} else if (downstreamFormat == null || !downstreamFormat.id.equals(mediaChunk.format.id)) {
if (downstreamFormat == null || !downstreamFormat.equals(mediaChunk.format)) {
notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger, notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger,
mediaChunk.startTimeUs); mediaChunk.startTimeUs);
MediaFormat format = mediaChunk.getMediaFormat();
chunkSource.getMaxVideoDimensions(format);
formatHolder.format = format;
formatHolder.drmInitData = mediaChunk.getPsshInfo();
downstreamFormat = mediaChunk.format; 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; return FORMAT_READ;
} }

View File

@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer.chunk; package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.util.Assertions;
import java.util.Comparator; import java.util.Comparator;
/** /**
@ -97,7 +99,7 @@ public class Format {
*/ */
public Format(String id, String mimeType, int width, int height, int numChannels, public Format(String id, String mimeType, int width, int height, int numChannels,
int audioSamplingRate, int bandwidth) { int audioSamplingRate, int bandwidth) {
this.id = id; this.id = Assertions.checkNotNull(id);
this.mimeType = mimeType; this.mimeType = mimeType;
this.width = width; this.width = width;
this.height = height; this.height = height;
@ -106,4 +108,19 @@ public class Format {
this.bandwidth = bandwidth; 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);
}
} }

View File

@ -87,8 +87,22 @@ public abstract class MediaChunk extends Chunk {
*/ */
public abstract boolean seekTo(long positionUs, boolean allowNoop); public abstract boolean seekTo(long positionUs, boolean allowNoop);
/**
* Prepares the chunk for reading. Does nothing if the chunk is already prepared.
* <p>
* 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. * Reads the next media sample from the chunk.
* <p>
* Should only be called after the chunk has been successfully prepared.
* *
* @param holder A holder to store the read sample. * @param holder A holder to store the read sample.
* @return True if a sample was read. False if more data is still required. * @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. * Returns the media format of the samples contained within this chunk.
* <p>
* Should only be called after the chunk has been successfully prepared.
* *
* @return The sample media format. * @return The sample media format.
*/ */
@ -106,6 +122,8 @@ public abstract class MediaChunk extends Chunk {
/** /**
* Returns the pssh information associated with the chunk. * Returns the pssh information associated with the chunk.
* <p>
* Should only be called after the chunk has been successfully prepared.
* *
* @return The pssh information. * @return The pssh information.
*/ */

View File

@ -33,24 +33,34 @@ import java.util.UUID;
public final class Mp4MediaChunk extends MediaChunk { public final class Mp4MediaChunk extends MediaChunk {
private final FragmentedMp4Extractor extractor; private final FragmentedMp4Extractor extractor;
private final boolean maybeSelfContained;
private final long sampleOffsetUs; private final long sampleOffsetUs;
private boolean prepared;
private MediaFormat mediaFormat;
private Map<UUID, byte[]> psshInfo;
/** /**
* @param dataSource A {@link DataSource} for loading the data. * @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded. * @param dataSpec Defines the data to be loaded.
* @param format The format of the stream to which this chunk belongs. * @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 trigger The reason for this chunk being selected.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @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 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 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, public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, FragmentedMp4Extractor extractor, long startTimeUs, long endTimeUs, int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex,
long sampleOffsetUs, int nextChunkIndex) { FragmentedMp4Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) {
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex); super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
this.extractor = extractor; this.extractor = extractor;
this.maybeSelfContained = maybeSelfContained;
this.sampleOffsetUs = sampleOffsetUs; this.sampleOffsetUs = sampleOffsetUs;
} }
@ -70,6 +80,29 @@ public final class Mp4MediaChunk extends MediaChunk {
return isDiscontinuous; 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 @Override
public boolean read(SampleHolder holder) throws ParserException { public boolean read(SampleHolder holder) throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); NonBlockingInputStream inputStream = getNonBlockingInputStream();
@ -84,12 +117,12 @@ public final class Mp4MediaChunk extends MediaChunk {
@Override @Override
public MediaFormat getMediaFormat() { public MediaFormat getMediaFormat() {
return extractor.getFormat(); return mediaFormat;
} }
@Override @Override
public Map<UUID, byte[]> getPsshInfo() { public Map<UUID, byte[]> getPsshInfo() {
return extractor.getPsshInfo(); return psshInfo;
} }
} }

View File

@ -77,6 +77,11 @@ public class SingleSampleMediaChunk extends MediaChunk {
this.headerData = headerData; this.headerData = headerData;
} }
@Override
public boolean prepare() {
return true;
}
@Override @Override
public boolean read(SampleHolder holder) { public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); NonBlockingInputStream inputStream = getNonBlockingInputStream();

View File

@ -64,6 +64,11 @@ public final class WebmMediaChunk extends MediaChunk {
return isDiscontinuous; return isDiscontinuous;
} }
@Override
public boolean prepare() {
return true;
}
@Override @Override
public boolean read(SampleHolder holder) { public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); NonBlockingInputStream inputStream = getNonBlockingInputStream();

View File

@ -231,8 +231,8 @@ public class DashMp4ChunkSource implements ChunkSource {
DataSpec dataSpec = new DataSpec(representation.uri, offset, size, DataSpec dataSpec = new DataSpec(representation.uri, offset, size,
representation.getCacheKey()); representation.getCacheKey());
return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, extractor, return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs,
startTimeUs, endTimeUs, 0, nextIndex); endTimeUs, nextIndex, extractor, false, 0);
} }
private static class InitializationMp4Loadable extends Chunk { private static class InitializationMp4Loadable extends Chunk {

View File

@ -83,9 +83,13 @@ public final class FragmentedMp4Extractor {
* A sidx atom was read. The parsed data can be read using {@link #getSegmentIndex()}. * A sidx atom was read. The parsed data can be read using {@link #getSegmentIndex()}.
*/ */
public static final int RESULT_READ_SIDX = 32; 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 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[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = 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}; 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. * in subsequent calls until the whole sample has been read.
* *
* @param inputStream The input stream from which data should be 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. * @return One or more of the {@code RESULT_*} flags defined in this class.
* @throws ParserException If an error occurs parsing the media data. * @throws ParserException If an error occurs parsing the media data.
*/ */
@ -1142,6 +1147,9 @@ public final class FragmentedMp4Extractor {
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
private int readSample(NonBlockingInputStream inputStream, SampleHolder out) { private int readSample(NonBlockingInputStream inputStream, SampleHolder out) {
if (out == null) {
return RESULT_NEED_SAMPLE_HOLDER;
}
int sampleSize = fragmentRun.sampleSizeTable[sampleIndex]; int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
ByteBuffer outputData = out.data; ByteBuffer outputData = out.data;
if (parserState == STATE_READING_SAMPLE_START) { if (parserState == STATE_READING_SAMPLE_START) {

View File

@ -36,7 +36,7 @@ import java.util.concurrent.TimeUnit;
* More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>. * More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>.
*/ */
@TargetApi(16) @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 DOC_TYPE_WEBM = "webm";
private static final String CODEC_ID_VP9 = "V_VP9"; private static final String CODEC_ID_VP9 = "V_VP9";
@ -104,7 +104,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
/* package */ DefaultWebmExtractor(EbmlReader reader) { /* package */ DefaultWebmExtractor(EbmlReader reader) {
this.reader = reader; this.reader = reader;
this.reader.setEventHandler(this); this.reader.setEventHandler(new InnerEbmlEventHandler());
this.cueTimesUs = new LongArray(); this.cueTimesUs = new LongArray();
this.cueClusterPositions = new LongArray(); this.cueClusterPositions = new LongArray();
} }
@ -150,8 +150,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
return format; return format;
} }
@Override /* package */ int getElementType(int id) {
public int getElementType(int id) {
switch (id) { switch (id) {
case ID_EBML: case ID_EBML:
case ID_SEGMENT: case ID_SEGMENT:
@ -185,8 +184,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
} }
} }
@Override /* package */ boolean onMasterElementStart(
public boolean onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) { int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
switch (id) { switch (id) {
case ID_SEGMENT: case ID_SEGMENT:
@ -205,8 +203,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
return true; return true;
} }
@Override /* package */ boolean onMasterElementEnd(int id) {
public boolean onMasterElementEnd(int id) {
if (id == ID_CUES) { if (id == ID_CUES) {
finishPreparing(); finishPreparing();
return false; return false;
@ -214,8 +211,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
return true; return true;
} }
@Override /* package */ boolean onIntegerElement(int id, long value) {
public boolean onIntegerElement(int id, long value) {
switch (id) { switch (id) {
case ID_EBML_READ_VERSION: case ID_EBML_READ_VERSION:
// Validate that EBMLReadVersion is supported. This extractor only supports v1. // Validate that EBMLReadVersion is supported. This extractor only supports v1.
@ -253,16 +249,14 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
return true; return true;
} }
@Override /* package */ boolean onFloatElement(int id, double value) {
public boolean onFloatElement(int id, double value) {
if (id == ID_DURATION) { if (id == ID_DURATION) {
durationUs = scaleTimecodeToUs((long) value); durationUs = scaleTimecodeToUs((long) value);
} }
return true; return true;
} }
@Override /* package */ boolean onStringElement(int id, String value) {
public boolean onStringElement(int id, String value) {
switch (id) { switch (id) {
case ID_DOC_TYPE: case ID_DOC_TYPE:
// Validate that DocType is supported. This extractor only supports "webm". // Validate that DocType is supported. This extractor only supports "webm".
@ -282,8 +276,7 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
return true; return true;
} }
@Override /* package */ boolean onBinaryElement(
public boolean onBinaryElement(
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes, int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
NonBlockingInputStream inputStream) { NonBlockingInputStream inputStream) {
if (id == ID_SIMPLE_BLOCK) { if (id == ID_SIMPLE_BLOCK) {
@ -383,4 +376,52 @@ public final class DefaultWebmExtractor implements WebmExtractor, EbmlEventHandl
prepared = true; 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);
}
}
} }

View File

@ -235,8 +235,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey); DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey);
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. // 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. // To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs.
return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, extractor, return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, chunkStartTimeUs,
chunkStartTimeUs, nextStartTimeUs, -chunkStartTimeUs, nextChunkIndex); nextStartTimeUs, nextChunkIndex, extractor, false, -chunkStartTimeUs);
} }
private static byte[] getKeyId(byte[] initData) { private static byte[] getKeyId(byte[] initData) {