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:
parent
16fe6a809e
commit
4366afc273
@ -54,8 +54,8 @@ public interface SampleSource {
|
||||
* Prepares the source.
|
||||
* <p>
|
||||
* 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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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.
|
||||
* <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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* Should only be called after the chunk has been successfully prepared.
|
||||
*
|
||||
* @return The pssh information.
|
||||
*/
|
||||
|
@ -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<UUID, byte[]> 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<UUID, byte[]> getPsshInfo() {
|
||||
return extractor.getPsshInfo();
|
||||
return psshInfo;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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>.
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user