Make DefaultWebmExtractor handle cues and format independently.

* Remove concept of being prepared by simply reporting if format
and/or cues are known.
* Allow replacement of format and/or cues later in the stream.
* Initialization and index segments can be parsed independently
of one another but must be in order due to internal WebM dependencies.
* Let seekTo() work even when cues are unknown.
This commit is contained in:
Oliver Woodman 2014-08-01 15:53:08 +01:00
parent 32464e6de4
commit 2a82ff353b

View File

@ -83,7 +83,6 @@ public final class DefaultWebmExtractor implements WebmExtractor {
private SampleHolder tempSampleHolder; private SampleHolder tempSampleHolder;
private boolean sampleRead; private boolean sampleRead;
private boolean prepared = false;
private long segmentStartOffsetBytes = UNKNOWN; private long segmentStartOffsetBytes = UNKNOWN;
private long segmentEndOffsetBytes = UNKNOWN; private long segmentEndOffsetBytes = UNKNOWN;
private long timecodeScale = 1000000L; private long timecodeScale = 1000000L;
@ -105,13 +104,11 @@ public final class DefaultWebmExtractor implements WebmExtractor {
/* package */ DefaultWebmExtractor(EbmlReader reader) { /* package */ DefaultWebmExtractor(EbmlReader reader) {
this.reader = reader; this.reader = reader;
this.reader.setEventHandler(new InnerEbmlEventHandler()); this.reader.setEventHandler(new InnerEbmlEventHandler());
this.cueTimesUs = new LongArray();
this.cueClusterPositions = new LongArray();
} }
@Override @Override
public boolean isPrepared() { public boolean isPrepared() {
return prepared; return format != null && cues != null;
} }
@Override @Override
@ -125,8 +122,9 @@ public final class DefaultWebmExtractor implements WebmExtractor {
@Override @Override
public boolean seekTo(long seekTimeUs, boolean allowNoop) { public boolean seekTo(long seekTimeUs, boolean allowNoop) {
checkPrepared();
if (allowNoop if (allowNoop
&& cues != null
&& clusterTimecodeUs != UNKNOWN
&& simpleBlockTimecodeUs != UNKNOWN && simpleBlockTimecodeUs != UNKNOWN
&& seekTimeUs >= simpleBlockTimecodeUs) { && seekTimeUs >= simpleBlockTimecodeUs) {
int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs); int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
@ -134,19 +132,19 @@ public final class DefaultWebmExtractor implements WebmExtractor {
return false; return false;
} }
} }
clusterTimecodeUs = UNKNOWN;
simpleBlockTimecodeUs = UNKNOWN;
reader.reset(); reader.reset();
return true; return true;
} }
@Override @Override
public SegmentIndex getCues() { public SegmentIndex getCues() {
checkPrepared();
return cues; return cues;
} }
@Override @Override
public MediaFormat getFormat() { public MediaFormat getFormat() {
checkPrepared();
return format; return format;
} }
@ -196,6 +194,8 @@ public final class DefaultWebmExtractor implements WebmExtractor {
break; break;
case ID_CUES: case ID_CUES:
cuesSizeBytes = headerSizeBytes + contentsSizeBytes; cuesSizeBytes = headerSizeBytes + contentsSizeBytes;
cueTimesUs = new LongArray();
cueClusterPositions = new LongArray();
break; break;
default: default:
// pass // pass
@ -204,11 +204,16 @@ public final class DefaultWebmExtractor implements WebmExtractor {
} }
/* package */ boolean onMasterElementEnd(int id) { /* package */ boolean onMasterElementEnd(int id) {
if (id == ID_CUES) { switch (id) {
finishPreparing(); case ID_CUES:
return false; buildCues();
return false;
case ID_VIDEO:
buildFormat();
return true;
default:
return true;
} }
return true;
} }
/* package */ boolean onIntegerElement(int id, long value) { /* package */ boolean onIntegerElement(int id, long value) {
@ -330,30 +335,40 @@ public final class DefaultWebmExtractor implements WebmExtractor {
return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale); return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
} }
private void checkPrepared() { /**
if (!prepared) { * Build a video {@link MediaFormat} containing recently gathered Video information, if needed.
throw new IllegalStateException("Parser not yet prepared"); *
* <p>Replaces the previous {@link #format} only if video width/height have changed.
* {@link #format} is guaranteed to not be null after calling this method. In
* the event that it can't be built, an {@link IllegalStateException} will be thrown.
*/
private void buildFormat() {
if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN
&& (format == null || format.width != pixelWidth || format.height != pixelHeight)) {
format = MediaFormat.createVideoFormat(
MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
} else if (format == null) {
throw new IllegalStateException("Unable to build format");
} }
} }
private void finishPreparing() { /**
if (prepared) { * Build a {@link SegmentIndex} containing recently gathered Cues information.
throw new IllegalStateException("Already prepared"); *
} else if (segmentStartOffsetBytes == UNKNOWN) { * <p>{@link #cues} is guaranteed to not be null after calling this method. In
* the event that it can't be built, an {@link IllegalStateException} will be thrown.
*/
private void buildCues() {
if (segmentStartOffsetBytes == UNKNOWN) {
throw new IllegalStateException("Segment start/end offsets unknown"); throw new IllegalStateException("Segment start/end offsets unknown");
} else if (durationUs == UNKNOWN) { } else if (durationUs == UNKNOWN) {
throw new IllegalStateException("Duration unknown"); throw new IllegalStateException("Duration unknown");
} else if (pixelWidth == UNKNOWN || pixelHeight == UNKNOWN) {
throw new IllegalStateException("Pixel width/height unknown");
} else if (cuesSizeBytes == UNKNOWN) { } else if (cuesSizeBytes == UNKNOWN) {
throw new IllegalStateException("Cues size unknown"); throw new IllegalStateException("Cues size unknown");
} else if (cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) { } else if (cueTimesUs == null || cueClusterPositions == null
|| cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
throw new IllegalStateException("Invalid/missing cue points"); throw new IllegalStateException("Invalid/missing cue points");
} }
format = MediaFormat.createVideoFormat(
MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
int cuePointsSize = cueTimesUs.size(); int cuePointsSize = cueTimesUs.size();
int[] sizes = new int[cuePointsSize]; int[] sizes = new int[cuePointsSize];
long[] offsets = new long[cuePointsSize]; long[] offsets = new long[cuePointsSize];
@ -372,8 +387,6 @@ 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;
prepared = true;
} }
/** /**