Changes around renderer readiness and extraction.

- Make MediaCodecTrackRenderer.isReady more permissive.
  This largely fixes #21
- Bring WebmExtractor closer to FragmentedMp4Extractor.
  The two will probably be placed under a common interface
  fairly soon, which will allow significant code
  deduplication.
This commit is contained in:
Oliver Woodman 2014-08-01 15:56:26 +01:00
parent 41ff1e4071
commit 9a124120ff
19 changed files with 317 additions and 269 deletions

View File

@ -67,7 +67,7 @@ public final class FrameworkSampleSource implements SampleSource {
extractor = new MediaExtractor(); extractor = new MediaExtractor();
extractor.setDataSource(context, uri, headers); extractor.setDataSource(context, uri, headers);
trackStates = new int[extractor.getTrackCount()]; trackStates = new int[extractor.getTrackCount()];
pendingDiscontinuities = new boolean[extractor.getTrackCount()]; pendingDiscontinuities = new boolean[trackStates.length];
trackInfos = new TrackInfo[trackStates.length]; trackInfos = new TrackInfo[trackStates.length];
for (int i = 0; i < trackStates.length; i++) { for (int i = 0; i < trackStates.length; i++) {
android.media.MediaFormat format = extractor.getTrackFormat(i); android.media.MediaFormat format = extractor.getTrackFormat(i);
@ -84,7 +84,7 @@ public final class FrameworkSampleSource implements SampleSource {
@Override @Override
public int getTrackCount() { public int getTrackCount() {
Assertions.checkState(prepared); Assertions.checkState(prepared);
return extractor.getTrackCount(); return trackStates.length;
} }
@Override @Override
@ -97,17 +97,18 @@ public final class FrameworkSampleSource implements SampleSource {
public void enable(int track, long timeUs) { public void enable(int track, long timeUs) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED); Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED);
boolean wasSourceEnabled = isEnabled();
trackStates[track] = TRACK_STATE_ENABLED; trackStates[track] = TRACK_STATE_ENABLED;
extractor.selectTrack(track); extractor.selectTrack(track);
if (!wasSourceEnabled) { seekToUs(timeUs);
seekToUs(timeUs);
}
} }
@Override @Override
public void continueBuffering(long playbackPositionUs) { public boolean continueBuffering(long playbackPositionUs) {
// Do nothing. The MediaExtractor instance is responsible for buffering. // MediaExtractor takes care of buffering and blocks until it has samples, so we can always
// return true here. Although note that the blocking behavior is itself as bug, as per the
// TODO further up this file. This method will need to return something else as part of fixing
// the TODO.
return true;
} }
@Override @Override
@ -122,15 +123,15 @@ public final class FrameworkSampleSource implements SampleSource {
if (onlyReadDiscontinuity) { if (onlyReadDiscontinuity) {
return NOTHING_READ; return NOTHING_READ;
} }
if (trackStates[track] != TRACK_STATE_FORMAT_SENT) {
formatHolder.format = MediaFormat.createFromFrameworkMediaFormatV16(
extractor.getTrackFormat(track));
formatHolder.drmInitData = Util.SDK_INT >= 18 ? getPsshInfoV18() : null;
trackStates[track] = TRACK_STATE_FORMAT_SENT;
return FORMAT_READ;
}
int extractorTrackIndex = extractor.getSampleTrackIndex(); int extractorTrackIndex = extractor.getSampleTrackIndex();
if (extractorTrackIndex == track) { if (extractorTrackIndex == track) {
if (trackStates[track] != TRACK_STATE_FORMAT_SENT) {
formatHolder.format = MediaFormat.createFromFrameworkMediaFormatV16(
extractor.getTrackFormat(track));
formatHolder.drmInitData = Util.SDK_INT >= 18 ? getPsshInfoV18() : null;
trackStates[track] = TRACK_STATE_FORMAT_SENT;
return FORMAT_READ;
}
if (sampleHolder.data != null) { if (sampleHolder.data != null) {
int offset = sampleHolder.data.position(); int offset = sampleHolder.data.position();
sampleHolder.size = extractor.readSampleData(sampleHolder.data, offset); sampleHolder.size = extractor.readSampleData(sampleHolder.data, offset);
@ -202,13 +203,4 @@ public final class FrameworkSampleSource implements SampleSource {
} }
} }
private boolean isEnabled() {
for (int i = 0; i < trackStates.length; i++) {
if (trackStates[i] != TRACK_STATE_DISABLED) {
return true;
}
}
return false;
}
} }

View File

@ -417,7 +417,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
@Override @Override
protected boolean isReady() { protected boolean isReady() {
return getPendingFrameCount() > 0; return super.isReady() || getPendingFrameCount() > 0;
} }
/** /**

View File

@ -128,6 +128,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
private int codecReconfigurationState; private int codecReconfigurationState;
private int trackIndex; private int trackIndex;
private boolean sourceIsReady;
private boolean inputStreamEnded; private boolean inputStreamEnded;
private boolean outputStreamEnded; private boolean outputStreamEnded;
private boolean waitingForKeys; private boolean waitingForKeys;
@ -196,6 +197,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
@Override @Override
protected void onEnabled(long timeUs, boolean joining) { protected void onEnabled(long timeUs, boolean joining) {
source.enable(trackIndex, timeUs); source.enable(trackIndex, timeUs);
sourceIsReady = false;
inputStreamEnded = false; inputStreamEnded = false;
outputStreamEnded = false; outputStreamEnded = false;
waitingForKeys = false; waitingForKeys = false;
@ -346,6 +348,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
protected void seekTo(long timeUs) throws ExoPlaybackException { protected void seekTo(long timeUs) throws ExoPlaybackException {
currentPositionUs = timeUs; currentPositionUs = timeUs;
source.seekToUs(timeUs); source.seekToUs(timeUs);
sourceIsReady = false;
inputStreamEnded = false; inputStreamEnded = false;
outputStreamEnded = false; outputStreamEnded = false;
waitingForKeys = false; waitingForKeys = false;
@ -364,7 +367,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
@Override @Override
protected void doSomeWork(long timeUs) throws ExoPlaybackException { protected void doSomeWork(long timeUs) throws ExoPlaybackException {
try { try {
source.continueBuffering(timeUs); sourceIsReady = source.continueBuffering(timeUs);
checkForDiscontinuity(); checkForDiscontinuity();
if (format == null) { if (format == null) {
readFormat(); readFormat();
@ -645,10 +648,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
@Override @Override
protected boolean isReady() { protected boolean isReady() {
return format != null && !waitingForKeys return format != null && !waitingForKeys
&& ((codec == null && !shouldInitCodec()) // We don't want the codec && (sourceIsReady || outputIndex >= 0 || isWithinHotswapPeriod());
|| outputIndex >= 0 // Or we have an output buffer ready to release
|| inputIndex < 0 // Or we don't have any input buffers to write to
|| isWithinHotswapPeriod()); // Or the codec is being hotswapped
} }
private boolean isWithinHotswapPeriod() { private boolean isWithinHotswapPeriod() {

View File

@ -235,7 +235,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
@Override @Override
protected boolean isReady() { protected boolean isReady() {
if (super.isReady() && (renderedFirstFrame || !codecInitialized())) { if (super.isReady()) {
// Ready. If we were joining then we've now joined, so clear the joining deadline. // Ready. If we were joining then we've now joined, so clear the joining deadline.
joiningDeadlineUs = -1; joiningDeadlineUs = -1;
return true; return true;

View File

@ -102,8 +102,11 @@ public interface SampleSource {
* Indicates to the source that it should still be buffering data. * Indicates to the source that it should still be buffering data.
* *
* @param playbackPositionUs The current playback position. * @param playbackPositionUs The current playback position.
* @return True if the source has available samples, or if the end of the stream has been reached.
* False if more data needs to be buffered for samples to become available.
* @throws IOException If an error occurred reading from the source.
*/ */
public void continueBuffering(long playbackPositionUs); public boolean continueBuffering(long playbackPositionUs) throws IOException;
/** /**
* Attempts to read either a sample, a new format or or a discontinuity from the source. * Attempts to read either a sample, a new format or or a discontinuity from the source.

View File

@ -246,11 +246,21 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
} }
@Override @Override
public void continueBuffering(long playbackPositionUs) { public boolean continueBuffering(long playbackPositionUs) throws IOException {
Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(state == STATE_ENABLED);
downstreamPositionUs = playbackPositionUs; downstreamPositionUs = playbackPositionUs;
chunkSource.continueBuffering(playbackPositionUs); chunkSource.continueBuffering(playbackPositionUs);
updateLoadControl(); updateLoadControl();
if (isPendingReset() || mediaChunks.isEmpty()) {
return false;
} else if (mediaChunks.getFirst().sampleAvailable()) {
// There's a sample available to be read from the current chunk.
return true;
} else {
// It may be the case that the current chunk has been fully read but not yet discarded and
// that the next chunk has an available sample. Return true if so, otherwise false.
return mediaChunks.size() > 1 && mediaChunks.get(1).sampleAvailable();
}
} }
@Override @Override

View File

@ -99,6 +99,14 @@ public abstract class MediaChunk extends Chunk {
*/ */
public abstract boolean prepare() throws ParserException; public abstract boolean prepare() throws ParserException;
/**
* Returns whether the next sample is available.
*
* @return True if the next sample is available for reading. False otherwise.
* @throws ParserException
*/
public abstract boolean sampleAvailable() throws ParserException;
/** /**
* Reads the next media sample from the chunk. * Reads the next media sample from the chunk.
* <p> * <p>

View File

@ -103,12 +103,19 @@ public final class Mp4MediaChunk extends MediaChunk {
return prepared; return prepared;
} }
@Override
public boolean sampleAvailable() throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
int result = extractor.read(inputStream, null);
return (result & FragmentedMp4Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
}
@Override @Override
public boolean read(SampleHolder holder) throws ParserException { public boolean read(SampleHolder holder) throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null); Assertions.checkState(inputStream != null);
int result = extractor.read(inputStream, holder); int result = extractor.read(inputStream, holder);
boolean sampleRead = (result & FragmentedMp4Extractor.RESULT_READ_SAMPLE_FULL) != 0; boolean sampleRead = (result & FragmentedMp4Extractor.RESULT_READ_SAMPLE) != 0;
if (sampleRead) { if (sampleRead) {
holder.timeUs -= sampleOffsetUs; holder.timeUs -= sampleOffsetUs;
} }

View File

@ -82,11 +82,16 @@ public class SingleSampleMediaChunk extends MediaChunk {
return true; return true;
} }
@Override
public boolean sampleAvailable() {
return isLoadFinished() && !isReadFinished();
}
@Override @Override
public boolean read(SampleHolder holder) { public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null); Assertions.checkState(inputStream != null);
if (!isLoadFinished()) { if (!sampleAvailable()) {
return false; return false;
} }
int bytesLoaded = (int) bytesLoaded(); int bytesLoaded = (int) bytesLoaded();

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer.chunk; package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.parser.webm.WebmExtractor; import com.google.android.exoplayer.parser.webm.WebmExtractor;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
@ -69,11 +70,19 @@ public final class WebmMediaChunk extends MediaChunk {
return true; return true;
} }
@Override
public boolean sampleAvailable() throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
int result = extractor.read(inputStream, null);
return (result & WebmExtractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
}
@Override @Override
public boolean read(SampleHolder holder) { public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null); Assertions.checkState(inputStream != null);
return extractor.read(inputStream, holder); int result = extractor.read(inputStream, holder);
return (result & WebmExtractor.RESULT_READ_SAMPLE) != 0;
} }
@Override @Override

View File

@ -146,7 +146,7 @@ public class DashMp4ChunkSource implements ChunkSource {
RangedUri pendingInitializationUri = null; RangedUri pendingInitializationUri = null;
RangedUri pendingIndexUri = null; RangedUri pendingIndexUri = null;
if (extractor.getTrack() == null) { if (extractor.getFormat() == null) {
pendingInitializationUri = selectedRepresentation.getInitializationUri(); pendingInitializationUri = selectedRepresentation.getInitializationUri();
} }
if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) { if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) {
@ -199,10 +199,10 @@ public class DashMp4ChunkSource implements ChunkSource {
if (initializationUri != null) { if (initializationUri != null) {
// It's common for initialization and index data to be stored adjacently. Attempt to merge // It's common for initialization and index data to be stored adjacently. Attempt to merge
// the two requests together to request both at once. // the two requests together to request both at once.
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_MOOV; expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INIT;
requestUri = initializationUri.attemptMerge(indexUri); requestUri = initializationUri.attemptMerge(indexUri);
if (requestUri != null) { if (requestUri != null) {
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_SIDX; expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX;
indexAnchor = indexUri.start + indexUri.length; indexAnchor = indexUri.start + indexUri.length;
} else { } else {
requestUri = initializationUri; requestUri = initializationUri;
@ -210,7 +210,7 @@ public class DashMp4ChunkSource implements ChunkSource {
} else { } else {
requestUri = indexUri; requestUri = indexUri;
indexAnchor = indexUri.start + indexUri.length; indexAnchor = indexUri.start + indexUri.length;
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_SIDX; expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX;
} }
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length, DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
representation.getCacheKey()); representation.getCacheKey());
@ -256,9 +256,9 @@ public class DashMp4ChunkSource implements ChunkSource {
throw new ParserException("Invalid extractor result. Expected " throw new ParserException("Invalid extractor result. Expected "
+ expectedExtractorResult + ", got " + result); + expectedExtractorResult + ", got " + result);
} }
if ((result & FragmentedMp4Extractor.RESULT_READ_SIDX) != 0) { if ((result & FragmentedMp4Extractor.RESULT_READ_INDEX) != 0) {
segmentIndexes.put(format.id, segmentIndexes.put(format.id,
new DashWrappingSegmentIndex(extractor.getSegmentIndex(), uri, indexAnchor)); new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor));
} }
} }

View File

@ -56,21 +56,26 @@ public class DashWebmChunkSource implements ChunkSource {
private final Format[] formats; private final Format[] formats;
private final HashMap<String, Representation> representations; private final HashMap<String, Representation> representations;
private final HashMap<String, WebmExtractor> extractors; private final HashMap<String, DefaultWebmExtractor> extractors;
private final HashMap<String, DashSegmentIndex> segmentIndexes; private final HashMap<String, DashSegmentIndex> segmentIndexes;
private boolean lastChunkWasInitialization; private boolean lastChunkWasInitialization;
/**
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param evaluator Selects from the available formats.
* @param representations The representations to be considered by the source.
*/
public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator, public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator,
Representation... representations) { Representation... representations) {
this.dataSource = dataSource; this.dataSource = dataSource;
this.evaluator = evaluator; this.evaluator = evaluator;
this.formats = new Format[representations.length]; this.formats = new Format[representations.length];
this.extractors = new HashMap<String, WebmExtractor>(); this.extractors = new HashMap<String, DefaultWebmExtractor>();
this.segmentIndexes = new HashMap<String, DashSegmentIndex>(); this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
this.representations = new HashMap<String, Representation>(); this.representations = new HashMap<String, Representation>();
this.trackInfo = new TrackInfo( this.trackInfo = new TrackInfo(representations[0].format.mimeType,
representations[0].format.mimeType, representations[0].periodDurationMs * 1000); representations[0].periodDurationMs * 1000);
this.evaluation = new Evaluation(); this.evaluation = new Evaluation();
int maxWidth = 0; int maxWidth = 0;
int maxHeight = 0; int maxHeight = 0;
@ -109,7 +114,7 @@ public class DashWebmChunkSource implements ChunkSource {
@Override @Override
public void disable(List<? extends MediaChunk> queue) { public void disable(List<? extends MediaChunk> queue) {
evaluator.disable(); evaluator.disable();
} }
@Override @Override
@ -140,13 +145,18 @@ public class DashWebmChunkSource implements ChunkSource {
Representation selectedRepresentation = representations.get(selectedFormat.id); Representation selectedRepresentation = representations.get(selectedFormat.id);
WebmExtractor extractor = extractors.get(selectedRepresentation.format.id); WebmExtractor extractor = extractors.get(selectedRepresentation.format.id);
if (!extractor.isPrepared()) { RangedUri pendingInitializationUri = null;
// TODO: This code forces cues to exist and to immediately follow the initialization RangedUri pendingIndexUri = null;
// data. Webm extractor should be generalized to allow cues to be optional. See [redacted]. if (extractor.getFormat() == null) {
RangedUri initializationUri = selectedRepresentation.getInitializationUri().attemptMerge( pendingInitializationUri = selectedRepresentation.getInitializationUri();
selectedRepresentation.getIndexUri()); }
Chunk initializationChunk = newInitializationChunk(initializationUri, selectedRepresentation, if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) {
extractor, dataSource, evaluation.trigger); pendingIndexUri = selectedRepresentation.getIndexUri();
}
if (pendingInitializationUri != null || pendingIndexUri != null) {
// We have initialization and/or index requests to make.
Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri,
selectedRepresentation, extractor, dataSource, evaluation.trigger);
lastChunkWasInitialization = true; lastChunkWasInitialization = true;
out.chunk = initializationChunk; out.chunk = initializationChunk;
return; return;
@ -181,12 +191,29 @@ public class DashWebmChunkSource implements ChunkSource {
// Do nothing. // Do nothing.
} }
private Chunk newInitializationChunk(RangedUri initializationUri, Representation representation, private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
WebmExtractor extractor, DataSource dataSource, int trigger) { Representation representation, WebmExtractor extractor, DataSource dataSource,
DataSpec dataSpec = new DataSpec(initializationUri.getUri(), initializationUri.start, int trigger) {
initializationUri.length, representation.getCacheKey()); int expectedExtractorResult = WebmExtractor.RESULT_END_OF_STREAM;
RangedUri requestUri;
if (initializationUri != null) {
// It's common for initialization and index data to be stored adjacently. Attempt to merge
// the two requests together to request both at once.
expectedExtractorResult |= WebmExtractor.RESULT_READ_INIT;
requestUri = initializationUri.attemptMerge(indexUri);
if (requestUri != null) {
expectedExtractorResult |= WebmExtractor.RESULT_READ_INDEX;
} else {
requestUri = initializationUri;
}
} else {
requestUri = indexUri;
expectedExtractorResult |= WebmExtractor.RESULT_READ_INDEX;
}
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
representation.getCacheKey());
return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format, return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format,
extractor); extractor, expectedExtractorResult);
} }
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex, private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
@ -206,22 +233,27 @@ public class DashWebmChunkSource implements ChunkSource {
private class InitializationWebmLoadable extends Chunk { private class InitializationWebmLoadable extends Chunk {
private final WebmExtractor extractor; private final WebmExtractor extractor;
private final int expectedExtractorResult;
private final Uri uri; private final Uri uri;
public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger, public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
Format format, WebmExtractor extractor) { Format format, WebmExtractor extractor, int expectedExtractorResult) {
super(dataSource, dataSpec, format, trigger); super(dataSource, dataSpec, format, trigger);
this.extractor = extractor; this.extractor = extractor;
this.expectedExtractorResult = expectedExtractorResult;
this.uri = dataSpec.uri; this.uri = dataSpec.uri;
} }
@Override @Override
protected void consumeStream(NonBlockingInputStream stream) throws IOException { protected void consumeStream(NonBlockingInputStream stream) throws IOException {
extractor.read(stream, null); int result = extractor.read(stream, null);
if (!extractor.isPrepared()) { if (result != expectedExtractorResult) {
throw new ParserException("Invalid initialization data"); throw new ParserException("Invalid extractor result. Expected "
+ expectedExtractorResult + ", got " + result);
}
if ((result & WebmExtractor.RESULT_READ_INDEX) != 0) {
segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getIndex(), uri, 0));
} }
segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getCues(), uri, 0));
} }
} }

View File

@ -59,7 +59,7 @@ public final class FragmentedMp4Extractor {
public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1; public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
/** /**
* An attempt to read from the input stream returned 0 bytes of data. * An attempt to read from the input stream returned insufficient data.
*/ */
public static final int RESULT_NEED_MORE_DATA = 1; public static final int RESULT_NEED_MORE_DATA = 1;
/** /**
@ -69,27 +69,23 @@ public final class FragmentedMp4Extractor {
/** /**
* A media sample was read. * A media sample was read.
*/ */
public static final int RESULT_READ_SAMPLE_FULL = 4; public static final int RESULT_READ_SAMPLE = 4;
/** /**
* A media sample was partially read. * A moov atom was read. The parsed data can be read using {@link #getFormat()} and
* {@link #getPsshInfo}.
*/ */
public static final int RESULT_READ_SAMPLE_PARTIAL = 8; public static final int RESULT_READ_INIT = 8;
/** /**
* A moov atom was read. The parsed data can be read using {@link #getTrack()}, * A sidx atom was read. The parsed data can be read using {@link #getIndex()}.
* {@link #getFormat()} and {@link #getPsshInfo}.
*/ */
public static final int RESULT_READ_MOOV = 16; public static final int RESULT_READ_INDEX = 16;
/**
* 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. * 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; public static final int RESULT_NEED_SAMPLE_HOLDER = 32;
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_NEED_SAMPLE_HOLDER; | RESULT_READ_SAMPLE | 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};
@ -98,8 +94,7 @@ public final class FragmentedMp4Extractor {
private static final int STATE_READING_ATOM_HEADER = 0; private static final int STATE_READING_ATOM_HEADER = 0;
private static final int STATE_READING_ATOM_PAYLOAD = 1; private static final int STATE_READING_ATOM_PAYLOAD = 1;
private static final int STATE_READING_CENC_AUXILIARY_DATA = 2; private static final int STATE_READING_CENC_AUXILIARY_DATA = 2;
private static final int STATE_READING_SAMPLE_START = 3; private static final int STATE_READING_SAMPLE = 3;
private static final int STATE_READING_SAMPLE_INCREMENTAL = 4;
// Atom data offsets // Atom data offsets
private static final int ATOM_HEADER_SIZE = 8; private static final int ATOM_HEADER_SIZE = 8;
@ -169,7 +164,6 @@ public final class FragmentedMp4Extractor {
private ParsableByteArray atomData; private ParsableByteArray atomData;
private ParsableByteArray cencAuxiliaryData; private ParsableByteArray cencAuxiliaryData;
private int cencAuxiliaryBytesRead; private int cencAuxiliaryBytesRead;
private int sampleBytesRead;
private int pendingSeekTimeMs; private int pendingSeekTimeMs;
private int sampleIndex; private int sampleIndex;
@ -207,7 +201,7 @@ public final class FragmentedMp4Extractor {
* *
* @return The segment index, or null if a SIDX atom has yet to be parsed. * @return The segment index, or null if a SIDX atom has yet to be parsed.
*/ */
public SegmentIndex getSegmentIndex() { public SegmentIndex getIndex() {
return segmentIndex; return segmentIndex;
} }
@ -245,17 +239,7 @@ public final class FragmentedMp4Extractor {
} }
/** /**
* Returns the track information parsed from the stream. * Sideloads track information into the extractor.
*
* @return The track, or null if a MOOV atom has yet to be parsed.
*/
public Track getTrack() {
return track;
}
/**
* Sideloads track information into the extractor, so that it can be read through
* {@link #getTrack()}.
* *
* @param track The track to sideload. * @param track The track to sideload.
*/ */
@ -270,10 +254,6 @@ public final class FragmentedMp4Extractor {
* The read terminates if the end of the input stream is reached, if an attempt to read from the * The read terminates if the end of the input stream is reached, if an attempt to read from the
* input stream returned 0 bytes of data, or if a sample is read. The returned flags indicate * input stream returned 0 bytes of data, or if a sample is read. The returned flags indicate
* both the reason for termination and data that was parsed during the read. * both the reason for termination and data that was parsed during the read.
* <p>
* If the returned flags include {@link #RESULT_READ_SAMPLE_PARTIAL} then the sample has been
* partially read into {@code out}. Hence the same {@link SampleHolder} instance must be passed
* 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 next sample should be read. If null then * @param out A {@link SampleHolder} into which the next sample should be read. If null then
@ -353,9 +333,6 @@ public final class FragmentedMp4Extractor {
case STATE_READING_CENC_AUXILIARY_DATA: case STATE_READING_CENC_AUXILIARY_DATA:
cencAuxiliaryBytesRead = 0; cencAuxiliaryBytesRead = 0;
break; break;
case STATE_READING_SAMPLE_START:
sampleBytesRead = 0;
break;
} }
parserState = state; parserState = state;
} }
@ -383,7 +360,7 @@ public final class FragmentedMp4Extractor {
enterState(STATE_READING_CENC_AUXILIARY_DATA); enterState(STATE_READING_CENC_AUXILIARY_DATA);
} else { } else {
cencAuxiliaryData = null; cencAuxiliaryData = null;
enterState(STATE_READING_SAMPLE_START); enterState(STATE_READING_SAMPLE);
} }
return 0; return 0;
} }
@ -442,7 +419,7 @@ public final class FragmentedMp4Extractor {
containerAtoms.peek().add(leaf); containerAtoms.peek().add(leaf);
} else if (leaf.type == Atom.TYPE_sidx) { } else if (leaf.type == Atom.TYPE_sidx) {
segmentIndex = parseSidx(leaf.getData()); segmentIndex = parseSidx(leaf.getData());
return RESULT_READ_SIDX; return RESULT_READ_INDEX;
} }
return 0; return 0;
} }
@ -450,7 +427,7 @@ public final class FragmentedMp4Extractor {
private int onContainerAtomRead(ContainerAtom container) { private int onContainerAtomRead(ContainerAtom container) {
if (container.type == Atom.TYPE_moov) { if (container.type == Atom.TYPE_moov) {
onMoovContainerAtomRead(container); onMoovContainerAtomRead(container);
return RESULT_READ_MOOV; return RESULT_READ_INIT;
} else if (container.type == Atom.TYPE_moof) { } else if (container.type == Atom.TYPE_moof) {
onMoofContainerAtomRead(container); onMoofContainerAtomRead(container);
} else if (!containerAtoms.isEmpty()) { } else if (!containerAtoms.isEmpty()) {
@ -1078,7 +1055,7 @@ public final class FragmentedMp4Extractor {
if (cencAuxiliaryBytesRead < length) { if (cencAuxiliaryBytesRead < length) {
return RESULT_NEED_MORE_DATA; return RESULT_NEED_MORE_DATA;
} }
enterState(STATE_READING_SAMPLE_START); enterState(STATE_READING_SAMPLE);
return 0; return 0;
} }
@ -1105,89 +1082,68 @@ public final class FragmentedMp4Extractor {
enterState(STATE_READING_ATOM_HEADER); enterState(STATE_READING_ATOM_HEADER);
return 0; return 0;
} }
if (sampleIndex < pendingSeekSyncSampleIndex) { int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
return skipSample(inputStream); if (inputStream.getAvailableByteCount() < sampleSize) {
return RESULT_NEED_MORE_DATA;
} }
return readSample(inputStream, out); if (sampleIndex < pendingSeekSyncSampleIndex) {
return skipSample(inputStream, sampleSize);
}
return readSample(inputStream, sampleSize, out);
} }
private int skipSample(NonBlockingInputStream inputStream) { private int skipSample(NonBlockingInputStream inputStream, int sampleSize) {
if (parserState == STATE_READING_SAMPLE_START) { ParsableByteArray sampleEncryptionData = cencAuxiliaryData != null ? cencAuxiliaryData
ParsableByteArray sampleEncryptionData = cencAuxiliaryData != null ? cencAuxiliaryData : fragmentRun.smoothStreamingSampleEncryptionData;
: fragmentRun.smoothStreamingSampleEncryptionData; if (sampleEncryptionData != null) {
if (sampleEncryptionData != null) { TrackEncryptionBox encryptionBox =
TrackEncryptionBox encryptionBox = track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex]; int vectorSize = encryptionBox.initializationVectorSize;
int vectorSize = encryptionBox.initializationVectorSize; boolean subsampleEncryption = cencAuxiliaryData != null
boolean subsampleEncryption = cencAuxiliaryData != null ? fragmentRun.auxiliarySampleInfoSizeTable[sampleIndex] > vectorSize
? fragmentRun.auxiliarySampleInfoSizeTable[sampleIndex] > vectorSize : fragmentRun.smoothStreamingUsesSubsampleEncryption;
: fragmentRun.smoothStreamingUsesSubsampleEncryption; sampleEncryptionData.skip(vectorSize);
sampleEncryptionData.skip(vectorSize); int subsampleCount = subsampleEncryption ? sampleEncryptionData.readUnsignedShort() : 1;
int subsampleCount = subsampleEncryption ? sampleEncryptionData.readUnsignedShort() : 1; if (subsampleEncryption) {
if (subsampleEncryption) { sampleEncryptionData.skip((2 + 4) * subsampleCount);
sampleEncryptionData.skip((2 + 4) * subsampleCount);
}
} }
} }
int sampleSize = fragmentRun.sampleSizeTable[sampleIndex]; inputStream.skip(sampleSize);
int bytesRead = inputStream.skip(sampleSize - sampleBytesRead);
if (bytesRead == -1) {
return RESULT_END_OF_STREAM;
}
sampleBytesRead += bytesRead;
if (sampleSize != sampleBytesRead) {
enterState(STATE_READING_SAMPLE_INCREMENTAL);
return RESULT_NEED_MORE_DATA;
}
sampleIndex++; sampleIndex++;
enterState(STATE_READING_SAMPLE_START); enterState(STATE_READING_SAMPLE);
return 0; return 0;
} }
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
private int readSample(NonBlockingInputStream inputStream, SampleHolder out) { private int readSample(NonBlockingInputStream inputStream, int sampleSize, SampleHolder out) {
if (out == null) { if (out == null) {
return RESULT_NEED_SAMPLE_HOLDER; return RESULT_NEED_SAMPLE_HOLDER;
} }
int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
ByteBuffer outputData = out.data; ByteBuffer outputData = out.data;
if (parserState == STATE_READING_SAMPLE_START) { out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L;
out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L; out.flags = 0;
out.flags = 0; if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) {
if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) { out.flags |= MediaExtractor.SAMPLE_FLAG_SYNC;
out.flags |= MediaExtractor.SAMPLE_FLAG_SYNC; lastSyncSampleIndex = sampleIndex;
lastSyncSampleIndex = sampleIndex; }
} if (out.allowDataBufferReplacement
if (out.allowDataBufferReplacement && (out.data == null || out.data.capacity() < sampleSize)) {
&& (out.data == null || out.data.capacity() < sampleSize)) { outputData = ByteBuffer.allocate(sampleSize);
outputData = ByteBuffer.allocate(sampleSize); out.data = outputData;
out.data = outputData; }
} ParsableByteArray sampleEncryptionData = cencAuxiliaryData != null ? cencAuxiliaryData
ParsableByteArray sampleEncryptionData = cencAuxiliaryData != null ? cencAuxiliaryData : fragmentRun.smoothStreamingSampleEncryptionData;
: fragmentRun.smoothStreamingSampleEncryptionData; if (sampleEncryptionData != null) {
if (sampleEncryptionData != null) { readSampleEncryptionData(sampleEncryptionData, out);
readSampleEncryptionData(sampleEncryptionData, out);
}
} }
int bytesRead;
if (outputData == null) { if (outputData == null) {
bytesRead = inputStream.skip(sampleSize - sampleBytesRead); inputStream.skip(sampleSize);
out.size = 0;
} else { } else {
bytesRead = inputStream.read(outputData, sampleSize - sampleBytesRead); inputStream.read(outputData, sampleSize);
}
if (bytesRead == -1) {
return RESULT_END_OF_STREAM;
}
sampleBytesRead += bytesRead;
if (sampleSize != sampleBytesRead) {
enterState(STATE_READING_SAMPLE_INCREMENTAL);
return RESULT_NEED_MORE_DATA | RESULT_READ_SAMPLE_PARTIAL;
}
if (outputData != null) {
if (track.type == Track.TYPE_VIDEO) { if (track.type == Track.TYPE_VIDEO) {
// The mp4 file contains length-prefixed NAL units, but the decoder wants start code // The mp4 file contains length-prefixed NAL units, but the decoder wants start code
// delimited content. Replace length prefixes with start codes. // delimited content. Replace length prefixes with start codes.
@ -1203,13 +1159,11 @@ public final class FragmentedMp4Extractor {
outputData.position(sampleOffset + sampleSize); outputData.position(sampleOffset + sampleSize);
} }
out.size = sampleSize; out.size = sampleSize;
} else {
out.size = 0;
} }
sampleIndex++; sampleIndex++;
enterState(STATE_READING_SAMPLE_START); enterState(STATE_READING_SAMPLE);
return RESULT_READ_SAMPLE_FULL; return RESULT_READ_SAMPLE;
} }
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")

View File

@ -138,9 +138,8 @@ import java.util.Stack;
while (true) { while (true) {
while (!masterElementsStack.isEmpty() while (!masterElementsStack.isEmpty()
&& bytesRead >= masterElementsStack.peek().elementEndOffsetBytes) { && bytesRead >= masterElementsStack.peek().elementEndOffsetBytes) {
if (!eventHandler.onMasterElementEnd(masterElementsStack.pop().elementId)) { eventHandler.onMasterElementEnd(masterElementsStack.pop().elementId);
return READ_RESULT_CONTINUE; return READ_RESULT_CONTINUE;
}
} }
if (state == STATE_BEGIN_READING) { if (state == STATE_BEGIN_READING) {
@ -161,12 +160,10 @@ import java.util.Stack;
case TYPE_MASTER: case TYPE_MASTER:
int masterHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max. int masterHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize)); masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize));
if (!eventHandler.onMasterElementStart( eventHandler.onMasterElementStart(elementId, elementOffset, masterHeaderSize,
elementId, elementOffset, masterHeaderSize, elementContentSize)) { elementContentSize);
prepareForNextElement(); prepareForNextElement();
return READ_RESULT_CONTINUE; return READ_RESULT_CONTINUE;
}
break;
case TYPE_UNSIGNED_INT: case TYPE_UNSIGNED_INT:
if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) { if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
throw new IllegalStateException("Invalid integer size " + elementContentSize); throw new IllegalStateException("Invalid integer size " + elementContentSize);
@ -177,11 +174,9 @@ import java.util.Stack;
return intResult; return intResult;
} }
long intValue = getTempByteArrayValue((int) elementContentSize, false); long intValue = getTempByteArrayValue((int) elementContentSize, false);
if (!eventHandler.onIntegerElement(elementId, intValue)) { eventHandler.onIntegerElement(elementId, intValue);
prepareForNextElement(); prepareForNextElement();
return READ_RESULT_CONTINUE; return READ_RESULT_CONTINUE;
}
break;
case TYPE_FLOAT: case TYPE_FLOAT:
if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
&& elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) { && elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
@ -199,11 +194,9 @@ import java.util.Stack;
} else { } else {
floatValue = Double.longBitsToDouble(valueBits); floatValue = Double.longBitsToDouble(valueBits);
} }
if (!eventHandler.onFloatElement(elementId, floatValue)) { eventHandler.onFloatElement(elementId, floatValue);
prepareForNextElement(); prepareForNextElement();
return READ_RESULT_CONTINUE; return READ_RESULT_CONTINUE;
}
break;
case TYPE_STRING: case TYPE_STRING:
if (elementContentSize > Integer.MAX_VALUE) { if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException( throw new IllegalStateException(
@ -219,11 +212,9 @@ import java.util.Stack;
} }
String stringValue = new String(stringBytes, Charset.forName("UTF-8")); String stringValue = new String(stringBytes, Charset.forName("UTF-8"));
stringBytes = null; stringBytes = null;
if (!eventHandler.onStringElement(elementId, stringValue)) { eventHandler.onStringElement(elementId, stringValue);
prepareForNextElement(); prepareForNextElement();
return READ_RESULT_CONTINUE; return READ_RESULT_CONTINUE;
}
break;
case TYPE_BINARY: case TYPE_BINARY:
if (elementContentSize > Integer.MAX_VALUE) { if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException( throw new IllegalStateException(
@ -233,18 +224,17 @@ import java.util.Stack;
return READ_RESULT_NEED_MORE_DATA; return READ_RESULT_NEED_MORE_DATA;
} }
int binaryHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max. int binaryHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
boolean keepGoing = eventHandler.onBinaryElement( boolean consumed = eventHandler.onBinaryElement(
elementId, elementOffset, binaryHeaderSize, (int) elementContentSize, inputStream); elementId, elementOffset, binaryHeaderSize, (int) elementContentSize, inputStream);
long expectedBytesRead = elementOffset + binaryHeaderSize + elementContentSize; if (consumed) {
if (expectedBytesRead != bytesRead) { long expectedBytesRead = elementOffset + binaryHeaderSize + elementContentSize;
throw new IllegalStateException("Incorrect total bytes read. Expected " if (expectedBytesRead != bytesRead) {
+ expectedBytesRead + " but actually " + bytesRead); throw new IllegalStateException("Incorrect total bytes read. Expected "
} + expectedBytesRead + " but actually " + bytesRead);
if (!keepGoing) { }
prepareForNextElement(); prepareForNextElement();
return READ_RESULT_CONTINUE;
} }
break; return READ_RESULT_CONTINUE;
case TYPE_UNKNOWN: case TYPE_UNKNOWN:
if (elementContentSize > Integer.MAX_VALUE) { if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException( throw new IllegalStateException(
@ -254,11 +244,11 @@ import java.util.Stack;
if (skipResult != READ_RESULT_CONTINUE) { if (skipResult != READ_RESULT_CONTINUE) {
return skipResult; return skipResult;
} }
prepareForNextElement();
break; break;
default: default:
throw new IllegalStateException("Invalid element type " + type); throw new IllegalStateException("Invalid element type " + type);
} }
prepareForNextElement();
} }
} }
@ -508,7 +498,7 @@ import java.util.Stack;
*/ */
private int updateBytesState(int additionalBytesRead, int totalBytes) { private int updateBytesState(int additionalBytesRead, int totalBytes) {
if (additionalBytesRead == -1) { if (additionalBytesRead == -1) {
return READ_RESULT_END_OF_FILE; return READ_RESULT_END_OF_STREAM;
} }
bytesState += additionalBytesRead; bytesState += additionalBytesRead;
bytesRead += additionalBytesRead; bytesRead += additionalBytesRead;

View File

@ -25,6 +25,7 @@ import com.google.android.exoplayer.util.MimeTypes;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.media.MediaExtractor; import android.media.MediaExtractor;
import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -77,11 +78,14 @@ public final class DefaultWebmExtractor implements WebmExtractor {
private static final int LACING_FIXED = 2; private static final int LACING_FIXED = 2;
private static final int LACING_EBML = 3; private static final int LACING_EBML = 3;
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
| RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
private final EbmlReader reader; private final EbmlReader reader;
private final byte[] simpleBlockTimecodeAndFlags = new byte[3]; private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
private SampleHolder tempSampleHolder; private SampleHolder sampleHolder;
private boolean sampleRead; private int readResults;
private long segmentStartOffsetBytes = UNKNOWN; private long segmentStartOffsetBytes = UNKNOWN;
private long segmentEndOffsetBytes = UNKNOWN; private long segmentEndOffsetBytes = UNKNOWN;
@ -107,17 +111,19 @@ public final class DefaultWebmExtractor implements WebmExtractor {
} }
@Override @Override
public boolean isPrepared() { public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
return format != null && cues != null; this.sampleHolder = sampleHolder;
} this.readResults = 0;
while ((readResults & READ_TERMINATING_RESULTS) == 0) {
@Override int ebmlReadResult = reader.read(inputStream);
public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) { if (ebmlReadResult == EbmlReader.READ_RESULT_NEED_MORE_DATA) {
tempSampleHolder = sampleHolder; readResults |= WebmExtractor.RESULT_NEED_MORE_DATA;
sampleRead = false; } else if (ebmlReadResult == EbmlReader.READ_RESULT_END_OF_STREAM) {
reader.read(inputStream); readResults |= WebmExtractor.RESULT_END_OF_STREAM;
tempSampleHolder = null; }
return sampleRead; }
this.sampleHolder = null;
return readResults;
} }
@Override @Override
@ -139,7 +145,7 @@ public final class DefaultWebmExtractor implements WebmExtractor {
} }
@Override @Override
public SegmentIndex getCues() { public SegmentIndex getIndex() {
return cues; return cues;
} }
@ -288,6 +294,12 @@ public final class DefaultWebmExtractor implements WebmExtractor {
// Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
// for info about how data is organized in a SimpleBlock element. // for info about how data is organized in a SimpleBlock element.
// If we don't have a sample holder then don't consume the data.
if (sampleHolder == null) {
readResults |= RESULT_NEED_SAMPLE_HOLDER;
return false;
}
// Value of trackNumber is not used but needs to be read. // Value of trackNumber is not used but needs to be read.
reader.readVarint(inputStream); reader.readVarint(inputStream);
@ -309,10 +321,10 @@ public final class DefaultWebmExtractor implements WebmExtractor {
case LACING_NONE: case LACING_NONE:
long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes; long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs; simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
tempSampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0; sampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
tempSampleHolder.decodeOnly = invisible; sampleHolder.decodeOnly = invisible;
tempSampleHolder.timeUs = clusterTimecodeUs + timecodeUs; sampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
tempSampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead()); sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
break; break;
case LACING_EBML: case LACING_EBML:
case LACING_FIXED: case LACING_FIXED:
@ -321,14 +333,22 @@ public final class DefaultWebmExtractor implements WebmExtractor {
throw new IllegalStateException("Lacing mode " + lacing + " not supported"); throw new IllegalStateException("Lacing mode " + lacing + " not supported");
} }
// Read video data into sample holder. ByteBuffer outputData = sampleHolder.data;
reader.readBytes(inputStream, tempSampleHolder.data, tempSampleHolder.size); if (sampleHolder.allowDataBufferReplacement
sampleRead = true; && (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size)) {
return false; outputData = ByteBuffer.allocate(sampleHolder.size);
} else { sampleHolder.data = outputData;
reader.skipBytes(inputStream, contentsSizeBytes); }
return true;
if (outputData == null) {
reader.skipBytes(inputStream, sampleHolder.size);
sampleHolder.size = 0;
} else {
reader.readBytes(inputStream, outputData, sampleHolder.size);
}
readResults |= RESULT_READ_SAMPLE;
} }
return true;
} }
private long scaleTimecodeToUs(long unscaledTimecode) { private long scaleTimecodeToUs(long unscaledTimecode) {
@ -347,6 +367,7 @@ public final class DefaultWebmExtractor implements WebmExtractor {
&& (format == null || format.width != pixelWidth || format.height != pixelHeight)) { && (format == null || format.width != pixelWidth || format.height != pixelHeight)) {
format = MediaFormat.createVideoFormat( format = MediaFormat.createVideoFormat(
MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null); MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
readResults |= RESULT_READ_INIT;
} else if (format == null) { } else if (format == null) {
throw new IllegalStateException("Unable to build format"); throw new IllegalStateException("Unable to build format");
} }
@ -387,6 +408,7 @@ public final class DefaultWebmExtractor implements WebmExtractor {
cues = new SegmentIndex((int) cuesSizeBytes, sizes, offsets, durationsUs, timesUs); cues = new SegmentIndex((int) cuesSizeBytes, sizes, offsets, durationsUs, timesUs);
cueTimesUs = null; cueTimesUs = null;
cueClusterPositions = null; cueClusterPositions = null;
readResults |= RESULT_READ_INDEX;
} }
/** /**
@ -401,30 +423,30 @@ public final class DefaultWebmExtractor implements WebmExtractor {
} }
@Override @Override
public boolean onMasterElementStart( public void onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) { int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
return DefaultWebmExtractor.this.onMasterElementStart( DefaultWebmExtractor.this.onMasterElementStart(
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes); id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
} }
@Override @Override
public boolean onMasterElementEnd(int id) { public void onMasterElementEnd(int id) {
return DefaultWebmExtractor.this.onMasterElementEnd(id); DefaultWebmExtractor.this.onMasterElementEnd(id);
} }
@Override @Override
public boolean onIntegerElement(int id, long value) { public void onIntegerElement(int id, long value) {
return DefaultWebmExtractor.this.onIntegerElement(id, value); DefaultWebmExtractor.this.onIntegerElement(id, value);
} }
@Override @Override
public boolean onFloatElement(int id, double value) { public void onFloatElement(int id, double value) {
return DefaultWebmExtractor.this.onFloatElement(id, value); DefaultWebmExtractor.this.onFloatElement(id, value);
} }
@Override @Override
public boolean onStringElement(int id, String value) { public void onStringElement(int id, String value) {
return DefaultWebmExtractor.this.onStringElement(id, value); DefaultWebmExtractor.this.onStringElement(id, value);
} }
@Override @Override

View File

@ -46,9 +46,8 @@ import java.nio.ByteBuffer;
* @param elementOffsetBytes The byte offset where this element starts * @param elementOffsetBytes The byte offset where this element starts
* @param headerSizeBytes The byte length of this element's ID and size header * @param headerSizeBytes The byte length of this element's ID and size header
* @param contentsSizeBytes The byte length of this element's children * @param contentsSizeBytes The byte length of this element's children
* @return {@code false} if parsing should stop right away
*/ */
public boolean onMasterElementStart( public void onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes); int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes);
/** /**
@ -56,44 +55,42 @@ import java.nio.ByteBuffer;
* {@link NonBlockingInputStream}. * {@link NonBlockingInputStream}.
* *
* @param id The integer ID of this element * @param id The integer ID of this element
* @return {@code false} if parsing should stop right away
*/ */
public boolean onMasterElementEnd(int id); public void onMasterElementEnd(int id);
/** /**
* Called when an integer element is encountered in the {@link NonBlockingInputStream}. * Called when an integer element is encountered in the {@link NonBlockingInputStream}.
* *
* @param id The integer ID of this element * @param id The integer ID of this element
* @param value The integer value this element contains * @param value The integer value this element contains
* @return {@code false} if parsing should stop right away
*/ */
public boolean onIntegerElement(int id, long value); public void onIntegerElement(int id, long value);
/** /**
* Called when a float element is encountered in the {@link NonBlockingInputStream}. * Called when a float element is encountered in the {@link NonBlockingInputStream}.
* *
* @param id The integer ID of this element * @param id The integer ID of this element
* @param value The float value this element contains * @param value The float value this element contains
* @return {@code false} if parsing should stop right away
*/ */
public boolean onFloatElement(int id, double value); public void onFloatElement(int id, double value);
/** /**
* Called when a string element is encountered in the {@link NonBlockingInputStream}. * Called when a string element is encountered in the {@link NonBlockingInputStream}.
* *
* @param id The integer ID of this element * @param id The integer ID of this element
* @param value The string value this element contains * @param value The string value this element contains
* @return {@code false} if parsing should stop right away
*/ */
public boolean onStringElement(int id, String value); public void onStringElement(int id, String value);
/** /**
* Called when a binary element is encountered in the {@link NonBlockingInputStream}. * Called when a binary element is encountered in the {@link NonBlockingInputStream}.
* *
* <p>The element header (containing element ID and content size) will already have been read. * <p>The element header (containing element ID and content size) will already have been read.
* Subclasses must exactly read the entire contents of the element, which is * Subclasses must either read nothing and return {@code false}, or exactly read the entire
* {@code contentsSizeBytes} in length. It's guaranteed that the full element contents will be * contents of the element, which is {@code contentsSizeBytes} in length, and return {@code true}.
* immediately available from {@code inputStream}. *
* <p>It's guaranteed that the full element contents will be immediately available from
* {@code inputStream}.
* *
* <p>Several methods in {@link EbmlReader} are available for reading the contents of a * <p>Several methods in {@link EbmlReader} are available for reading the contents of a
* binary element: * binary element:
@ -111,7 +108,7 @@ import java.nio.ByteBuffer;
* @param contentsSizeBytes The byte length of this element's contents * @param contentsSizeBytes The byte length of this element's contents
* @param inputStream The {@link NonBlockingInputStream} from which this * @param inputStream The {@link NonBlockingInputStream} from which this
* element's contents should be read * element's contents should be read
* @return {@code false} if parsing should stop right away * @return True if the element was read. False otherwise.
*/ */
public boolean onBinaryElement( public boolean onBinaryElement(
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes, int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,

View File

@ -44,12 +44,12 @@ import java.nio.ByteBuffer;
// Return values for reading methods. // Return values for reading methods.
public static final int READ_RESULT_CONTINUE = 0; public static final int READ_RESULT_CONTINUE = 0;
public static final int READ_RESULT_NEED_MORE_DATA = 1; public static final int READ_RESULT_NEED_MORE_DATA = 1;
public static final int READ_RESULT_END_OF_FILE = 2; public static final int READ_RESULT_END_OF_STREAM = 2;
public void setEventHandler(EbmlEventHandler eventHandler); public void setEventHandler(EbmlEventHandler eventHandler);
/** /**
* Reads from a {@link NonBlockingInputStream} and calls event callbacks as needed. * Reads from a {@link NonBlockingInputStream}, invoking an event callback if possible.
* *
* @param inputStream The input stream from which data should be read * @param inputStream The input stream from which data should be read
* @return One of the {@code RESULT_*} flags defined in this interface * @return One of the {@code RESULT_*} flags defined in this interface

View File

@ -30,24 +30,38 @@ import com.google.android.exoplayer.upstream.NonBlockingInputStream;
public interface WebmExtractor { public interface WebmExtractor {
/** /**
* Whether the has parsed the cues and sample format from the stream. * An attempt to read from the input stream returned insufficient data.
*
* @return True if the extractor is prepared. False otherwise
*/ */
public boolean isPrepared(); public static final int RESULT_NEED_MORE_DATA = 1;
/**
* The end of the input stream was reached.
*/
public static final int RESULT_END_OF_STREAM = 2;
/**
* A media sample was read.
*/
public static final int RESULT_READ_SAMPLE = 4;
/**
* Initialization data was read. The parsed data can be read using {@link #getFormat()}.
*/
public static final int RESULT_READ_INIT = 8;
/**
* A sidx atom was read. The parsed data can be read using {@link #getIndex()}.
*/
public static final int RESULT_READ_INDEX = 16;
/**
* The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
*/
public static final int RESULT_NEED_SAMPLE_HOLDER = 32;
/** /**
* Consumes data from a {@link NonBlockingInputStream}. * Consumes data from a {@link NonBlockingInputStream}.
* *
* <p>If the return value is {@code false}, then a sample may have been partially read into
* {@code sampleHolder}. Hence the same {@link SampleHolder} instance must be passed
* 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 sampleHolder A {@link SampleHolder} into which the sample should be read * @param sampleHolder A {@link SampleHolder} into which the sample should be read
* @return {@code true} if a sample has been read into the sample holder * @return One or more of the {@code RESULT_*} flags defined in this class.
*/ */
public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder); public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder);
/** /**
* Seeks to a position before or equal to the requested time. * Seeks to a position before or equal to the requested time.
@ -66,7 +80,7 @@ public interface WebmExtractor {
* @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet * @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet
* prepared * prepared
*/ */
public SegmentIndex getCues(); public SegmentIndex getIndex();
/** /**
* Returns the format of the samples contained within the media stream. * Returns the format of the samples contained within the media stream.

View File

@ -144,7 +144,12 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
@Override @Override
protected void doSomeWork(long timeUs) throws ExoPlaybackException { protected void doSomeWork(long timeUs) throws ExoPlaybackException {
source.continueBuffering(timeUs); try {
source.continueBuffering(timeUs);
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
currentPositionUs = timeUs; currentPositionUs = timeUs;
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we advance // We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we advance