Refactor WebM extractor.

This commit is contained in:
Oliver Woodman 2014-07-09 23:20:23 +01:00
parent 9e16dec2f8
commit 686ac2a6f5
6 changed files with 1122 additions and 822 deletions

View File

@ -29,6 +29,7 @@ import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.chunk.WebmMediaChunk;
import com.google.android.exoplayer.dash.mpd.Representation;
import com.google.android.exoplayer.parser.SegmentIndex;
import com.google.android.exoplayer.parser.webm.DefaultWebmExtractor;
import com.google.android.exoplayer.parser.webm.WebmExtractor;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
@ -85,7 +86,7 @@ public class DashWebmChunkSource implements ChunkSource {
formats[i] = representations[i].format;
maxWidth = Math.max(formats[i].width, maxWidth);
maxHeight = Math.max(formats[i].height, maxHeight);
extractors.put(formats[i].id, new WebmExtractor());
extractors.put(formats[i].id, new DefaultWebmExtractor());
this.representations.put(formats[i].id, representations[i]);
}
this.maxWidth = maxWidth;

View File

@ -0,0 +1,558 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.parser.webm;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Stack;
/**
* Default version of a basic event-driven incremental EBML parser which needs an
* {@link EbmlEventHandler} to define IDs/types and react to events.
*
* <p>EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers.
* It was originally designed for the Matroska container format. More information about EBML and
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
*/
/* package */ final class DefaultEbmlReader implements EbmlReader {
// State values used in variables state, elementIdState, elementContentSizeState, and
// varintBytesState.
private static final int STATE_BEGIN_READING = 0;
private static final int STATE_READ_CONTENTS = 1;
private static final int STATE_FINISHED_READING = 2;
/**
* The first byte of a variable-length integer (varint) will have one of these bit masks
* indicating the total length in bytes.
*
* <p>{@code 0x80} is a one-byte integer, {@code 0x40} is two bytes, and so on up to eight bytes.
*/
private static final int[] VARINT_LENGTH_MASKS = new int[] {
0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01
};
private static final int MAX_INTEGER_ELEMENT_SIZE_BYTES = 8;
private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4;
private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;
/**
* Scratch space to read in EBML varints, unsigned ints, and floats - each of which can be
* up to 8 bytes.
*/
private final byte[] tempByteArray = new byte[8];
private final Stack<MasterElement> masterElementsStack = new Stack<MasterElement>();
/**
* Current {@link EbmlEventHandler} which is queried for element types
* and informed of element events.
*/
private EbmlEventHandler eventHandler;
/**
* Overall state for the current element. Must be one of the {@code STATE_*} constants.
*/
private int state;
/**
* Total bytes read since starting or the last {@link #reset()}.
*/
private long bytesRead;
/**
* The starting byte offset of the current element being parsed.
*/
private long elementOffset;
/**
* Holds the current element ID after {@link #elementIdState} is {@link #STATE_FINISHED_READING}.
*/
private int elementId;
/**
* State for the ID of the current element. Must be one of the {@code STATE_*} constants.
*/
private int elementIdState;
/**
* Holds the current element content size after {@link #elementContentSizeState}
* is {@link #STATE_FINISHED_READING}.
*/
private long elementContentSize;
/**
* State for the content size of the current element.
* Must be one of the {@code STATE_*} constants.
*/
private int elementContentSizeState;
/**
* State for the current variable-length integer (varint) being read into
* {@link #tempByteArray}. Must be one of the {@code STATE_*} constants.
*/
private int varintBytesState;
/**
* Length in bytes of the current variable-length integer (varint) being read into
* {@link #tempByteArray}.
*/
private int varintBytesLength;
/**
* Counts the number of bytes being contiguously read into either {@link #tempByteArray} or
* {@link #stringBytes}. Used to determine when all required bytes have been read across
* multiple calls.
*/
private int bytesState;
/**
* Holds string element bytes as they're being read in. Allocated after the element content
* size is known and released after calling {@link EbmlEventHandler#onStringElement(int, String)}.
*/
private byte[] stringBytes;
@Override
public void setEventHandler(EbmlEventHandler eventHandler) {
this.eventHandler = eventHandler;
}
@Override
public int read(NonBlockingInputStream inputStream) {
Assertions.checkState(eventHandler != null);
while (true) {
while (!masterElementsStack.isEmpty()
&& bytesRead >= masterElementsStack.peek().elementEndOffsetBytes) {
if (!eventHandler.onMasterElementEnd(masterElementsStack.pop().elementId)) {
return READ_RESULT_CONTINUE;
}
}
if (state == STATE_BEGIN_READING) {
int idResult = readElementId(inputStream);
if (idResult != READ_RESULT_CONTINUE) {
return idResult;
}
int sizeResult = readElementContentSize(inputStream);
if (sizeResult != READ_RESULT_CONTINUE) {
return sizeResult;
}
state = STATE_READ_CONTENTS;
bytesState = 0;
}
int type = eventHandler.getElementType(elementId);
switch (type) {
case TYPE_MASTER:
int masterHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize));
if (!eventHandler.onMasterElementStart(
elementId, elementOffset, masterHeaderSize, elementContentSize)) {
prepareForNextElement();
return READ_RESULT_CONTINUE;
}
break;
case TYPE_UNSIGNED_INT:
if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
throw new IllegalStateException("Invalid integer size " + elementContentSize);
}
int intResult =
readBytesInternal(inputStream, tempByteArray, (int) elementContentSize);
if (intResult != READ_RESULT_CONTINUE) {
return intResult;
}
long intValue = getTempByteArrayValue((int) elementContentSize, false);
if (!eventHandler.onIntegerElement(elementId, intValue)) {
prepareForNextElement();
return READ_RESULT_CONTINUE;
}
break;
case TYPE_FLOAT:
if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
&& elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
throw new IllegalStateException("Invalid float size " + elementContentSize);
}
int floatResult =
readBytesInternal(inputStream, tempByteArray, (int) elementContentSize);
if (floatResult != READ_RESULT_CONTINUE) {
return floatResult;
}
long valueBits = getTempByteArrayValue((int) elementContentSize, false);
double floatValue;
if (elementContentSize == VALID_FLOAT32_ELEMENT_SIZE_BYTES) {
floatValue = Float.intBitsToFloat((int) valueBits);
} else {
floatValue = Double.longBitsToDouble(valueBits);
}
if (!eventHandler.onFloatElement(elementId, floatValue)) {
prepareForNextElement();
return READ_RESULT_CONTINUE;
}
break;
case TYPE_STRING:
if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException(
"String element size " + elementContentSize + " is larger than MAX_INT");
}
if (stringBytes == null) {
stringBytes = new byte[(int) elementContentSize];
}
int stringResult =
readBytesInternal(inputStream, stringBytes, (int) elementContentSize);
if (stringResult != READ_RESULT_CONTINUE) {
return stringResult;
}
String stringValue = new String(stringBytes, Charset.forName("UTF-8"));
stringBytes = null;
if (!eventHandler.onStringElement(elementId, stringValue)) {
prepareForNextElement();
return READ_RESULT_CONTINUE;
}
break;
case TYPE_BINARY:
if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException(
"Binary element size " + elementContentSize + " is larger than MAX_INT");
}
if (inputStream.getAvailableByteCount() < elementContentSize) {
return READ_RESULT_NEED_MORE_DATA;
}
int binaryHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
boolean keepGoing = eventHandler.onBinaryElement(
elementId, elementOffset, binaryHeaderSize, (int) elementContentSize, inputStream);
long expectedBytesRead = elementOffset + binaryHeaderSize + elementContentSize;
if (expectedBytesRead != bytesRead) {
throw new IllegalStateException("Incorrect total bytes read. Expected "
+ expectedBytesRead + " but actually " + bytesRead);
}
if (!keepGoing) {
prepareForNextElement();
return READ_RESULT_CONTINUE;
}
break;
case TYPE_UNKNOWN:
if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException(
"Unknown element size " + elementContentSize + " is larger than MAX_INT");
}
int skipResult = skipBytesInternal(inputStream, (int) elementContentSize);
if (skipResult != READ_RESULT_CONTINUE) {
return skipResult;
}
break;
default:
throw new IllegalStateException("Invalid element type " + type);
}
prepareForNextElement();
}
}
@Override
public long getBytesRead() {
return bytesRead;
}
@Override
public void reset() {
prepareForNextElement();
masterElementsStack.clear();
bytesRead = 0;
}
@Override
public long readVarint(NonBlockingInputStream inputStream) {
varintBytesState = STATE_BEGIN_READING;
int result = readVarintBytes(inputStream);
if (result != READ_RESULT_CONTINUE) {
throw new IllegalStateException("Couldn't read varint");
}
return getTempByteArrayValue(varintBytesLength, true);
}
@Override
public void readBytes(NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
bytesState = 0;
int result = readBytesInternal(inputStream, byteBuffer, totalBytes);
if (result != READ_RESULT_CONTINUE) {
throw new IllegalStateException("Couldn't read bytes into buffer");
}
}
@Override
public void readBytes(NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
bytesState = 0;
int result = readBytesInternal(inputStream, byteArray, totalBytes);
if (result != READ_RESULT_CONTINUE) {
throw new IllegalStateException("Couldn't read bytes into array");
}
}
@Override
public void skipBytes(NonBlockingInputStream inputStream, int totalBytes) {
bytesState = 0;
int result = skipBytesInternal(inputStream, totalBytes);
if (result != READ_RESULT_CONTINUE) {
throw new IllegalStateException("Couldn't skip bytes");
}
}
/**
* Resets the internal state of {@link #read(NonBlockingInputStream)} so that it can start
* reading a new element from scratch.
*/
private void prepareForNextElement() {
state = STATE_BEGIN_READING;
elementIdState = STATE_BEGIN_READING;
elementContentSizeState = STATE_BEGIN_READING;
elementOffset = bytesRead;
}
/**
* Reads an element ID such that reading can be stopped and started again in a later call
* if not enough bytes are available. Returns {@link #READ_RESULT_CONTINUE} if a full element ID
* has been read into {@link #elementId}. Reset {@link #elementIdState} to
* {@link #STATE_BEGIN_READING} before calling to indicate a new element ID should be read.
*
* @param inputStream The input stream from which an element ID should be read
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int readElementId(NonBlockingInputStream inputStream) {
if (elementIdState == STATE_FINISHED_READING) {
return READ_RESULT_CONTINUE;
}
if (elementIdState == STATE_BEGIN_READING) {
varintBytesState = STATE_BEGIN_READING;
elementIdState = STATE_READ_CONTENTS;
}
int result = readVarintBytes(inputStream);
if (result != READ_RESULT_CONTINUE) {
return result;
}
// Element IDs are at most 4 bytes so cast to int now.
elementId = (int) getTempByteArrayValue(varintBytesLength, false);
elementIdState = STATE_FINISHED_READING;
return READ_RESULT_CONTINUE;
}
/**
* Reads an element's content size such that reading can be stopped and started again in a later
* call if not enough bytes are available.
*
* <p>Returns {@link #READ_RESULT_CONTINUE} if an entire element size has been
* read into {@link #elementContentSize}. Reset {@link #elementContentSizeState} to
* {@link #STATE_BEGIN_READING} before calling to indicate a new element size should be read.
*
* @param inputStream The input stream from which an element size should be read
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int readElementContentSize(NonBlockingInputStream inputStream) {
if (elementContentSizeState == STATE_FINISHED_READING) {
return READ_RESULT_CONTINUE;
}
if (elementContentSizeState == STATE_BEGIN_READING) {
varintBytesState = STATE_BEGIN_READING;
elementContentSizeState = STATE_READ_CONTENTS;
}
int result = readVarintBytes(inputStream);
if (result != READ_RESULT_CONTINUE) {
return result;
}
elementContentSize = getTempByteArrayValue(varintBytesLength, true);
elementContentSizeState = STATE_FINISHED_READING;
return READ_RESULT_CONTINUE;
}
/**
* Reads an EBML variable-length integer (varint) such that reading can be stopped and started
* again in a later call if not enough bytes are available.
*
* <p>Returns {@link #READ_RESULT_CONTINUE} if an entire varint has been read into
* {@link #tempByteArray} and the length of the varint is in {@link #varintBytesLength}.
* Reset {@link #varintBytesState} to {@link #STATE_BEGIN_READING} before calling to indicate
* a new varint should be read.
*
* @param inputStream The input stream from which a varint should be read
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int readVarintBytes(NonBlockingInputStream inputStream) {
if (varintBytesState == STATE_FINISHED_READING) {
return READ_RESULT_CONTINUE;
}
// Read first byte to get length.
if (varintBytesState == STATE_BEGIN_READING) {
bytesState = 0;
int result = readBytesInternal(inputStream, tempByteArray, 1);
if (result != READ_RESULT_CONTINUE) {
return result;
}
varintBytesState = STATE_READ_CONTENTS;
int firstByte = tempByteArray[0] & 0xff;
varintBytesLength = -1;
for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
varintBytesLength = i + 1;
break;
}
}
if (varintBytesLength == -1) {
throw new IllegalStateException(
"No valid varint length mask found at bytesRead = " + bytesRead);
}
}
// Read remaining bytes.
int result = readBytesInternal(inputStream, tempByteArray, varintBytesLength);
if (result != READ_RESULT_CONTINUE) {
return result;
}
// All bytes have been read.
return READ_RESULT_CONTINUE;
}
/**
* Reads a set amount of bytes into a {@link ByteBuffer} such that reading can be stopped
* and started again later if not enough bytes are available.
*
* <p>Returns {@link #READ_RESULT_CONTINUE} if all bytes have been read. Reset
* {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
*
* @param inputStream The input stream from which bytes should be read
* @param byteBuffer The {@link ByteBuffer} into which bytes should be read
* @param totalBytes The total size of bytes to be read
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int readBytesInternal(
NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
if (bytesState == STATE_BEGIN_READING && totalBytes > byteBuffer.capacity()) {
throw new IllegalArgumentException("Byte buffer not large enough");
}
if (bytesState >= totalBytes) {
return READ_RESULT_CONTINUE;
}
int remainingBytes = totalBytes - bytesState;
int additionalBytesRead = inputStream.read(byteBuffer, remainingBytes);
return updateBytesState(additionalBytesRead, totalBytes);
}
/**
* Reads a set amount of bytes into a {@code byte[]} such that reading can be stopped
* and started again later if not enough bytes are available.
*
* <p>Returns {@link #READ_RESULT_CONTINUE} if all bytes have been read. Reset
* {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
*
* @param inputStream The input stream from which bytes should be read
* @param byteArray The {@code byte[]} into which bytes should be read
* @param totalBytes The total size of bytes to be read
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int readBytesInternal(
NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
if (bytesState == STATE_BEGIN_READING && totalBytes > byteArray.length) {
throw new IllegalArgumentException("Byte array not large enough");
}
if (bytesState >= totalBytes) {
return READ_RESULT_CONTINUE;
}
int remainingBytes = totalBytes - bytesState;
int additionalBytesRead = inputStream.read(byteArray, bytesState, remainingBytes);
return updateBytesState(additionalBytesRead, totalBytes);
}
/**
* Skips a set amount of bytes such that reading can be stopped and started again later if
* not enough bytes are available.
*
* <p>Returns {@link #READ_RESULT_CONTINUE} if all bytes have been skipped. Reset
* {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes
* should be skipped.
*
* @param inputStream The input stream from which bytes should be skipped
* @param totalBytes The total size of bytes to be skipped
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int skipBytesInternal(NonBlockingInputStream inputStream, int totalBytes) {
if (bytesState >= totalBytes) {
return READ_RESULT_CONTINUE;
}
int remainingBytes = totalBytes - bytesState;
int additionalBytesRead = inputStream.skip(remainingBytes);
return updateBytesState(additionalBytesRead, totalBytes);
}
/**
* Updates {@link #bytesState} and {@link #bytesRead} after reading bytes in one of the
* {@code verbBytesInternal} methods.
*
* @param additionalBytesRead The number of additional bytes read to be accounted for
* @param totalBytes The total size of bytes to be read or skipped
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int updateBytesState(int additionalBytesRead, int totalBytes) {
if (additionalBytesRead == -1) {
return READ_RESULT_END_OF_FILE;
}
bytesState += additionalBytesRead;
bytesRead += additionalBytesRead;
if (bytesState < totalBytes) {
return READ_RESULT_NEED_MORE_DATA;
} else {
return READ_RESULT_CONTINUE;
}
}
/**
* Parses and returns the integer value currently read into the first {@code byteLength} bytes
* of {@link #tempByteArray}. EBML varint length masks can optionally be removed.
*
* @param byteLength The number of bytes to parse from {@link #tempByteArray}
* @param removeLengthMask Removes the variable-length integer length mask from the value
* @return The resulting integer value. This value could be up to 8-bytes so a Java long is used
*/
private long getTempByteArrayValue(int byteLength, boolean removeLengthMask) {
if (removeLengthMask) {
tempByteArray[0] &= ~VARINT_LENGTH_MASKS[varintBytesLength - 1];
}
long varint = 0;
for (int i = 0; i < byteLength; i++) {
// Shift all existing bits up one byte and add the next byte at the bottom.
varint = (varint << 8) | (tempByteArray[i] & 0xff);
}
return varint;
}
/**
* Used in {@link #masterElementsStack} to track when the current master element ends so that
* {@link EbmlEventHandler#onMasterElementEnd(int)} is called.
*/
private static final class MasterElement {
private final int elementId;
private final long elementEndOffsetBytes;
private MasterElement(int elementId, long elementEndOffsetBytes) {
this.elementId = elementId;
this.elementEndOffsetBytes = elementEndOffsetBytes;
}
}
}

View File

@ -0,0 +1,386 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.parser.webm;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.parser.SegmentIndex;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.LongArray;
import com.google.android.exoplayer.util.MimeTypes;
import android.annotation.TargetApi;
import android.media.MediaExtractor;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* Default version of an extractor to facilitate data retrieval from the WebM container format.
*
* <p>WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
* 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 {
private static final String DOC_TYPE_WEBM = "webm";
private static final String CODEC_ID_VP9 = "V_VP9";
private static final int UNKNOWN = -1;
// Element IDs
private static final int ID_EBML = 0x1A45DFA3;
private static final int ID_EBML_READ_VERSION = 0x42F7;
private static final int ID_DOC_TYPE = 0x4282;
private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
private static final int ID_SEGMENT = 0x18538067;
private static final int ID_INFO = 0x1549A966;
private static final int ID_TIMECODE_SCALE = 0x2AD7B1;
private static final int ID_DURATION = 0x4489;
private static final int ID_CLUSTER = 0x1F43B675;
private static final int ID_TIME_CODE = 0xE7;
private static final int ID_SIMPLE_BLOCK = 0xA3;
private static final int ID_TRACKS = 0x1654AE6B;
private static final int ID_TRACK_ENTRY = 0xAE;
private static final int ID_CODEC_ID = 0x86;
private static final int ID_VIDEO = 0xE0;
private static final int ID_PIXEL_WIDTH = 0xB0;
private static final int ID_PIXEL_HEIGHT = 0xBA;
private static final int ID_CUES = 0x1C53BB6B;
private static final int ID_CUE_POINT = 0xBB;
private static final int ID_CUE_TIME = 0xB3;
private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
// SimpleBlock Lacing Values
private static final int LACING_NONE = 0;
private static final int LACING_XIPH = 1;
private static final int LACING_FIXED = 2;
private static final int LACING_EBML = 3;
private final EbmlReader reader;
private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
private SampleHolder tempSampleHolder;
private boolean sampleRead;
private boolean prepared = false;
private long segmentStartOffsetBytes = UNKNOWN;
private long segmentEndOffsetBytes = UNKNOWN;
private long timecodeScale = 1000000L;
private long durationUs = UNKNOWN;
private int pixelWidth = UNKNOWN;
private int pixelHeight = UNKNOWN;
private long cuesSizeBytes = UNKNOWN;
private long clusterTimecodeUs = UNKNOWN;
private long simpleBlockTimecodeUs = UNKNOWN;
private MediaFormat format;
private SegmentIndex cues;
private LongArray cueTimesUs;
private LongArray cueClusterPositions;
public DefaultWebmExtractor() {
this(new DefaultEbmlReader());
}
/* package */ DefaultWebmExtractor(EbmlReader reader) {
this.reader = reader;
this.reader.setEventHandler(this);
this.cueTimesUs = new LongArray();
this.cueClusterPositions = new LongArray();
}
@Override
public boolean isPrepared() {
return prepared;
}
@Override
public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
tempSampleHolder = sampleHolder;
sampleRead = false;
reader.read(inputStream);
tempSampleHolder = null;
return sampleRead;
}
@Override
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
checkPrepared();
if (allowNoop
&& simpleBlockTimecodeUs != UNKNOWN
&& seekTimeUs >= simpleBlockTimecodeUs) {
int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) {
return false;
}
}
reader.reset();
return true;
}
@Override
public SegmentIndex getCues() {
checkPrepared();
return cues;
}
@Override
public MediaFormat getFormat() {
checkPrepared();
return format;
}
@Override
public int getElementType(int id) {
switch (id) {
case ID_EBML:
case ID_SEGMENT:
case ID_INFO:
case ID_CLUSTER:
case ID_TRACKS:
case ID_TRACK_ENTRY:
case ID_VIDEO:
case ID_CUES:
case ID_CUE_POINT:
case ID_CUE_TRACK_POSITIONS:
return EbmlReader.TYPE_MASTER;
case ID_EBML_READ_VERSION:
case ID_DOC_TYPE_READ_VERSION:
case ID_TIMECODE_SCALE:
case ID_TIME_CODE:
case ID_PIXEL_WIDTH:
case ID_PIXEL_HEIGHT:
case ID_CUE_TIME:
case ID_CUE_CLUSTER_POSITION:
return EbmlReader.TYPE_UNSIGNED_INT;
case ID_DOC_TYPE:
case ID_CODEC_ID:
return EbmlReader.TYPE_STRING;
case ID_SIMPLE_BLOCK:
return EbmlReader.TYPE_BINARY;
case ID_DURATION:
return EbmlReader.TYPE_FLOAT;
default:
return EbmlReader.TYPE_UNKNOWN;
}
}
@Override
public boolean onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
switch (id) {
case ID_SEGMENT:
if (segmentStartOffsetBytes != UNKNOWN || segmentEndOffsetBytes != UNKNOWN) {
throw new IllegalStateException("Multiple Segment elements not supported");
}
segmentStartOffsetBytes = elementOffsetBytes + headerSizeBytes;
segmentEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
break;
case ID_CUES:
cuesSizeBytes = headerSizeBytes + contentsSizeBytes;
break;
default:
// pass
}
return true;
}
@Override
public boolean onMasterElementEnd(int id) {
if (id == ID_CUES) {
finishPreparing();
return false;
}
return true;
}
@Override
public boolean onIntegerElement(int id, long value) {
switch (id) {
case ID_EBML_READ_VERSION:
// Validate that EBMLReadVersion is supported. This extractor only supports v1.
if (value != 1) {
throw new IllegalArgumentException("EBMLReadVersion " + value + " not supported");
}
break;
case ID_DOC_TYPE_READ_VERSION:
// Validate that DocTypeReadVersion is supported. This extractor only supports up to v2.
if (value < 1 || value > 2) {
throw new IllegalArgumentException("DocTypeReadVersion " + value + " not supported");
}
break;
case ID_TIMECODE_SCALE:
timecodeScale = value;
break;
case ID_PIXEL_WIDTH:
pixelWidth = (int) value;
break;
case ID_PIXEL_HEIGHT:
pixelHeight = (int) value;
break;
case ID_CUE_TIME:
cueTimesUs.add(scaleTimecodeToUs(value));
break;
case ID_CUE_CLUSTER_POSITION:
cueClusterPositions.add(value);
break;
case ID_TIME_CODE:
clusterTimecodeUs = scaleTimecodeToUs(value);
break;
default:
// pass
}
return true;
}
@Override
public boolean onFloatElement(int id, double value) {
if (id == ID_DURATION) {
durationUs = scaleTimecodeToUs((long) value);
}
return true;
}
@Override
public boolean onStringElement(int id, String value) {
switch (id) {
case ID_DOC_TYPE:
// Validate that DocType is supported. This extractor only supports "webm".
if (!DOC_TYPE_WEBM.equals(value)) {
throw new IllegalArgumentException("DocType " + value + " not supported");
}
break;
case ID_CODEC_ID:
// Validate that CodecID is supported. This extractor only supports "V_VP9".
if (!CODEC_ID_VP9.equals(value)) {
throw new IllegalArgumentException("CodecID " + value + " not supported");
}
break;
default:
// pass
}
return true;
}
@Override
public boolean onBinaryElement(
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
NonBlockingInputStream inputStream) {
if (id == ID_SIMPLE_BLOCK) {
// Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
// for info about how data is organized in a SimpleBlock element.
// Value of trackNumber is not used but needs to be read.
reader.readVarint(inputStream);
// Next three bytes have timecode and flags.
reader.readBytes(inputStream, simpleBlockTimecodeAndFlags, 3);
// First two bytes of the three are the relative timecode.
int timecode =
(simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff);
long timecodeUs = scaleTimecodeToUs(timecode);
// Last byte of the three has some flags and the lacing value.
boolean keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80;
boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
// Validate lacing and set info into sample holder.
switch (lacing) {
case LACING_NONE:
long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
tempSampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
tempSampleHolder.decodeOnly = invisible;
tempSampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
tempSampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
break;
case LACING_EBML:
case LACING_FIXED:
case LACING_XIPH:
default:
throw new IllegalStateException("Lacing mode " + lacing + " not supported");
}
// Read video data into sample holder.
reader.readBytes(inputStream, tempSampleHolder.data, tempSampleHolder.size);
sampleRead = true;
return false;
} else {
reader.skipBytes(inputStream, contentsSizeBytes);
return true;
}
}
private long scaleTimecodeToUs(long unscaledTimecode) {
return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
}
private void checkPrepared() {
if (!prepared) {
throw new IllegalStateException("Parser not yet prepared");
}
}
private void finishPreparing() {
if (prepared) {
throw new IllegalStateException("Already prepared");
} else if (segmentStartOffsetBytes == UNKNOWN) {
throw new IllegalStateException("Segment start/end offsets unknown");
} else if (durationUs == UNKNOWN) {
throw new IllegalStateException("Duration unknown");
} else if (pixelWidth == UNKNOWN || pixelHeight == UNKNOWN) {
throw new IllegalStateException("Pixel width/height unknown");
} else if (cuesSizeBytes == UNKNOWN) {
throw new IllegalStateException("Cues size unknown");
} else if (cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
throw new IllegalStateException("Invalid/missing cue points");
}
format = MediaFormat.createVideoFormat(
MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
int cuePointsSize = cueTimesUs.size();
int[] sizes = new int[cuePointsSize];
long[] offsets = new long[cuePointsSize];
long[] durationsUs = new long[cuePointsSize];
long[] timesUs = new long[cuePointsSize];
for (int i = 0; i < cuePointsSize; i++) {
timesUs[i] = cueTimesUs.get(i);
offsets[i] = segmentStartOffsetBytes + cueClusterPositions.get(i);
}
for (int i = 0; i < cuePointsSize - 1; i++) {
sizes[i] = (int) (offsets[i + 1] - offsets[i]);
durationsUs[i] = timesUs[i + 1] - timesUs[i];
}
sizes[cuePointsSize - 1] = (int) (segmentEndOffsetBytes - offsets[cuePointsSize - 1]);
durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
cues = new SegmentIndex((int) cuesSizeBytes, sizes, offsets, durationsUs, timesUs);
cueTimesUs = null;
cueClusterPositions = null;
prepared = true;
}
}

View File

@ -0,0 +1,119 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.parser.webm;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import java.nio.ByteBuffer;
/**
* Defines EBML element IDs/types and reacts to events.
*/
/* package */ interface EbmlEventHandler {
/**
* Retrieves the type of an element ID.
*
* <p>If {@link EbmlReader#TYPE_UNKNOWN} is returned then the element is skipped.
* Note that all children of a skipped master element are also skipped.
*
* @param id The integer ID of this element
* @return One of the {@code TYPE_} constants defined in this class
*/
public int getElementType(int id);
/**
* Called when a master element is encountered in the {@link NonBlockingInputStream}.
*
* <p>Following events should be considered as taking place "within" this element until a
* matching call to {@link #onMasterElementEnd(int)} is made. Note that it is possible for
* another master element of the same ID to be nested within itself.
*
* @param id The integer ID of this element
* @param elementOffsetBytes The byte offset where this element starts
* @param headerSizeBytes The byte length of this element's ID and size header
* @param contentsSizeBytes The byte length of this element's children
* @return {@code false} if parsing should stop right away
*/
public boolean onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes);
/**
* Called when a master element has finished reading in all of its children from the
* {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @return {@code false} if parsing should stop right away
*/
public boolean onMasterElementEnd(int id);
/**
* Called when an integer element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @param value The integer value this element contains
* @return {@code false} if parsing should stop right away
*/
public boolean onIntegerElement(int id, long value);
/**
* Called when a float element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @param value The float value this element contains
* @return {@code false} if parsing should stop right away
*/
public boolean onFloatElement(int id, double value);
/**
* Called when a string element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @param value The string value this element contains
* @return {@code false} if parsing should stop right away
*/
public boolean onStringElement(int id, String value);
/**
* 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.
* Subclasses must exactly read the entire contents of the element, which is
* {@code contentsSizeBytes} in length. 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
* binary element:
* <ul>
* <li>{@link EbmlReader#readVarint(NonBlockingInputStream)}.
* <li>{@link EbmlReader#readBytes(NonBlockingInputStream, byte[], int)}.
* <li>{@link EbmlReader#readBytes(NonBlockingInputStream, ByteBuffer, int)}.
* <li>{@link EbmlReader#skipBytes(NonBlockingInputStream, int)}.
* <li>{@link EbmlReader#getBytesRead()}.
*
* @param id The integer ID of this element
* @param elementOffsetBytes The byte offset where this element starts
* @param headerSizeBytes The byte length of this element's ID and size header
* @param contentsSizeBytes The byte length of this element's contents
* @param inputStream The {@link NonBlockingInputStream} from which this
* element's contents should be read
* @return {@code false} if parsing should stop right away
*/
public boolean onBinaryElement(
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
NonBlockingInputStream inputStream);
}

View File

@ -16,528 +16,92 @@
package com.google.android.exoplayer.parser.webm;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Stack;
/**
* An event-driven incremental EBML reader base class.
* Basic event-driven incremental EBML parser which needs an {@link EbmlEventHandler} to
* define IDs/types and react to events.
*
* <p>EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers.
* It was originally designed for the Matroska container format. More information about EBML and
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
*/
public abstract class EbmlReader {
/* package */ interface EbmlReader {
// Element Types
protected static final int TYPE_UNKNOWN = 0; // Undefined element.
protected static final int TYPE_MASTER = 1; // Contains child elements.
protected static final int TYPE_UNSIGNED_INT = 2;
protected static final int TYPE_STRING = 3;
protected static final int TYPE_BINARY = 4;
protected static final int TYPE_FLOAT = 5;
/** Undefined element. */
public static final int TYPE_UNKNOWN = 0;
/** Contains child elements. */
public static final int TYPE_MASTER = 1;
/** Unsigned integer value of up to 8 bytes. */
public static final int TYPE_UNSIGNED_INT = 2;
public static final int TYPE_STRING = 3;
public static final int TYPE_BINARY = 4;
/** IEEE floating point value of either 4 or 8 bytes. */
public static final int TYPE_FLOAT = 5;
// Return values for methods read, readElementId, readElementSize, readVarintBytes, and readBytes.
protected static final int RESULT_CONTINUE = 0;
protected static final int RESULT_NEED_MORE_DATA = 1;
protected static final int RESULT_END_OF_FILE = 2;
// Return values for reading methods.
public static final int READ_RESULT_CONTINUE = 0;
public static final int READ_RESULT_NEED_MORE_DATA = 1;
public static final int READ_RESULT_END_OF_FILE = 2;
// State values used in variables state, elementIdState, elementContentSizeState, and
// varintBytesState.
private static final int STATE_BEGIN_READING = 0;
private static final int STATE_READ_CONTENTS = 1;
private static final int STATE_FINISHED_READING = 2;
/**
* The first byte of a variable-length integer (varint) will have one of these bit masks
* indicating the total length in bytes. {@code 0x80} is a one-byte integer,
* {@code 0x40} is two bytes, and so on up to eight bytes.
*/
private static final int[] VARINT_LENGTH_MASKS = new int[] {
0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01
};
private final Stack<MasterElement> masterElementsStack = new Stack<MasterElement>();
private final byte[] tempByteArray = new byte[8];
private int state;
private long bytesRead;
private long elementOffset;
private int elementId;
private int elementIdState;
private long elementContentSize;
private int elementContentSizeState;
private int varintBytesState;
private int varintBytesLength;
private int bytesState;
private byte[] stringBytes;
/**
* Called to retrieve the type of an element ID. If {@link #TYPE_UNKNOWN} is returned then
* the element is skipped. Note that all children of a skipped master element are also skipped.
*
* @param id The integer ID of this element.
* @return One of the {@code TYPE_} constants defined in this class.
*/
protected abstract int getElementType(int id);
/**
* Called when a master element is encountered in the {@link NonBlockingInputStream}.
* Following events should be considered as taking place "within" this element until a
* matching call to {@link #onMasterElementEnd(int)} is made. Note that it
* is possible for the same master element to be nested within itself.
*
* @param id The integer ID of this element.
* @param elementOffset The byte offset where this element starts.
* @param headerSize The byte length of this element's ID and size header.
* @param contentsSize The byte length of this element's children.
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
*/
protected abstract boolean onMasterElementStart(
int id, long elementOffset, int headerSize, int contentsSize);
/**
* Called when a master element has finished reading in all of its children from the
* {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element.
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
*/
protected abstract boolean onMasterElementEnd(int id);
/**
* Called when an integer element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element.
* @param value The integer value this element contains.
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
*/
protected abstract boolean onIntegerElement(int id, long value);
/**
* Called when a float element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element.
* @param value The float value this element contains.
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
*/
protected abstract boolean onFloatElement(int id, double value);
/**
* Called when a string element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element.
* @param value The string value this element contains.
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
*/
protected abstract boolean onStringElement(int id, String value);
/**
* Called when a binary element is encountered in the {@link NonBlockingInputStream}.
* 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 {@code contentsSize}
* bytes in length. It's guaranteed that the full element contents will be immediately available
* from {@code inputStream}.
*
* <p>Several methods are available for reading the contents of a binary element:
* <ul>
* <li>{@link #readVarint(NonBlockingInputStream)}.
* <li>{@link #readBytes(NonBlockingInputStream, byte[], int)}.
* <li>{@link #readBytes(NonBlockingInputStream, ByteBuffer, int)}.
* <li>{@link #skipBytes(NonBlockingInputStream, int)}.
* <li>{@link #getBytesRead()}.
*
* @param inputStream The {@link NonBlockingInputStream} from which this
* element's contents should be read.
* @param id The integer ID of this element.
* @param elementOffset The byte offset where this element starts.
* @param headerSize The byte length of this element's ID and size header.
* @param contentsSize The byte length of this element's contents.
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
*/
protected abstract boolean onBinaryElement(NonBlockingInputStream inputStream,
int id, long elementOffset, int headerSize, int contentsSize);
public void setEventHandler(EbmlEventHandler eventHandler);
/**
* Reads from a {@link NonBlockingInputStream} and calls event callbacks as needed.
*
* @param inputStream The input stream from which data should be read.
* @return One of the {@code RESULT_*} flags defined in this class.
* @param inputStream The input stream from which data should be read
* @return One of the {@code RESULT_*} flags defined in this interface
*/
protected final int read(NonBlockingInputStream inputStream) {
while (true) {
while (masterElementsStack.size() > 0
&& bytesRead >= masterElementsStack.peek().elementEndOffset) {
if (!onMasterElementEnd(masterElementsStack.pop().elementId)) {
return RESULT_CONTINUE;
}
}
if (state == STATE_BEGIN_READING) {
final int resultId = readElementId(inputStream);
if (resultId != RESULT_CONTINUE) {
return resultId;
}
final int resultSize = readElementContentSize(inputStream);
if (resultSize != RESULT_CONTINUE) {
return resultSize;
}
state = STATE_READ_CONTENTS;
bytesState = 0;
}
final int type = getElementType(elementId);
switch (type) {
case TYPE_MASTER:
final int masterHeaderSize = (int) (bytesRead - elementOffset);
masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize));
if (!onMasterElementStart(
elementId, elementOffset, masterHeaderSize, (int) elementContentSize)) {
prepareForNextElement();
return RESULT_CONTINUE;
}
break;
case TYPE_UNSIGNED_INT:
Assertions.checkState(elementContentSize <= 8);
final int resultInt =
readBytes(inputStream, null, tempByteArray, (int) elementContentSize);
if (resultInt != RESULT_CONTINUE) {
return resultInt;
}
final long intValue = parseTempByteArray((int) elementContentSize, false);
if (!onIntegerElement(elementId, intValue)) {
prepareForNextElement();
return RESULT_CONTINUE;
}
break;
case TYPE_FLOAT:
Assertions.checkState(elementContentSize == 4 || elementContentSize == 8);
final int resultFloat =
readBytes(inputStream, null, tempByteArray, (int) elementContentSize);
if (resultFloat != RESULT_CONTINUE) {
return resultFloat;
}
final long valueBits = parseTempByteArray((int) elementContentSize, false);
final double floatValue;
if (elementContentSize == 4) {
floatValue = Float.intBitsToFloat((int) valueBits);
} else {
floatValue = Double.longBitsToDouble(valueBits);
}
if (!onFloatElement(elementId, floatValue)) {
prepareForNextElement();
return RESULT_CONTINUE;
}
break;
case TYPE_STRING:
if (stringBytes == null) {
stringBytes = new byte[(int) elementContentSize];
}
final int resultString =
readBytes(inputStream, null, stringBytes, (int) elementContentSize);
if (resultString != RESULT_CONTINUE) {
return resultString;
}
final String stringValue = new String(stringBytes, Charset.forName("UTF-8"));
stringBytes = null;
if (!onStringElement(elementId, stringValue)) {
prepareForNextElement();
return RESULT_CONTINUE;
}
break;
case TYPE_BINARY:
if (inputStream.getAvailableByteCount() < elementContentSize) {
return RESULT_NEED_MORE_DATA;
}
final int binaryHeaderSize = (int) (bytesRead - elementOffset);
final boolean keepGoing = onBinaryElement(
inputStream, elementId, elementOffset, binaryHeaderSize, (int) elementContentSize);
Assertions.checkState(elementOffset + binaryHeaderSize + elementContentSize == bytesRead);
if (!keepGoing) {
prepareForNextElement();
return RESULT_CONTINUE;
}
break;
case TYPE_UNKNOWN:
// Unknown elements should be skipped.
Assertions.checkState(
readBytes(inputStream, null, null, (int) elementContentSize) == RESULT_CONTINUE);
break;
default:
throw new IllegalStateException("Invalid element type " + type);
}
prepareForNextElement();
}
}
public int read(NonBlockingInputStream inputStream);
/**
* @return The total number of bytes consumed by the reader since first created
* or last {@link #reset()}.
* The total number of bytes consumed by the reader since first created or last {@link #reset()}.
*/
protected final long getBytesRead() {
return bytesRead;
}
public long getBytesRead();
/**
* Resets the entire state of the reader so that it will read a new EBML structure from scratch.
* This includes resetting the value returned from {@link #getBytesRead()} to 0 and discarding
* all pending {@link #onMasterElementEnd(int)} events.
*
* <p>This includes resetting the value returned from {@link #getBytesRead()} to 0 and discarding
* all pending {@link EbmlEventHandler#onMasterElementEnd(int)} events.
*/
protected final void reset() {
prepareForNextElement();
masterElementsStack.clear();
bytesRead = 0;
}
public void reset();
/**
* Reads, parses, and returns an EBML variable-length integer (varint) from the contents
* of a binary element.
*
* @param inputStream The input stream from which data should be read.
* @return The varint value at the current position of the contents of a binary element.
* @param inputStream The input stream from which data should be read
* @return The varint value at the current position of the contents of a binary element
*/
protected final long readVarint(NonBlockingInputStream inputStream) {
varintBytesState = STATE_BEGIN_READING;
Assertions.checkState(readVarintBytes(inputStream) == RESULT_CONTINUE);
return parseTempByteArray(varintBytesLength, true);
}
public long readVarint(NonBlockingInputStream inputStream);
/**
* Reads a fixed number of bytes from the contents of a binary element into a {@link ByteBuffer}.
*
* @param inputStream The input stream from which data should be read.
* @param byteBuffer The {@link ByteBuffer} to which data should be written.
* @param totalBytes The fixed number of bytes to be read and written.
* @param inputStream The input stream from which data should be read
* @param byteBuffer The {@link ByteBuffer} to which data should be written
* @param totalBytes The fixed number of bytes to be read and written
*/
protected final void readBytes(
NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
bytesState = 0;
Assertions.checkState(readBytes(inputStream, byteBuffer, null, totalBytes) == RESULT_CONTINUE);
}
public void readBytes(NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes);
/**
* Reads a fixed number of bytes from the contents of a binary element into a {@code byte[]}.
*
* @param inputStream The input stream from which data should be read.
* @param byteArray The byte array to which data should be written.
* @param totalBytes The fixed number of bytes to be read and written.
* @param inputStream The input stream from which data should be read
* @param byteArray The byte array to which data should be written
* @param totalBytes The fixed number of bytes to be read and written
*/
protected final void readBytes(
NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
bytesState = 0;
Assertions.checkState(readBytes(inputStream, null, byteArray, totalBytes) == RESULT_CONTINUE);
}
public void readBytes(NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes);
/**
* Skips a fixed number of bytes from the contents of a binary element.
*
* @param inputStream The input stream from which data should be skipped.
* @param totalBytes The fixed number of bytes to be skipped.
* @param inputStream The input stream from which data should be skipped
* @param totalBytes The fixed number of bytes to be skipped
*/
protected final void skipBytes(NonBlockingInputStream inputStream, int totalBytes) {
bytesState = 0;
Assertions.checkState(readBytes(inputStream, null, null, totalBytes) == RESULT_CONTINUE);
}
/**
* Resets the internal state of {@link #read(NonBlockingInputStream)} so that it can start
* reading a new element from scratch.
*/
private final void prepareForNextElement() {
state = STATE_BEGIN_READING;
elementIdState = STATE_BEGIN_READING;
elementContentSizeState = STATE_BEGIN_READING;
elementOffset = bytesRead;
}
/**
* Reads an element ID such that reading can be stopped and started again in a later call
* if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if a full element ID
* has been read into {@link #elementId}. Reset {@link #elementIdState} to
* {@link #STATE_BEGIN_READING} before calling to indicate a new element ID should be read.
*
* @param inputStream The input stream from which an element ID should be read.
* @return One of the {@code RESULT_*} flags defined in this class.
*/
private int readElementId(NonBlockingInputStream inputStream) {
if (elementIdState == STATE_FINISHED_READING) {
return RESULT_CONTINUE;
}
if (elementIdState == STATE_BEGIN_READING) {
varintBytesState = STATE_BEGIN_READING;
elementIdState = STATE_READ_CONTENTS;
}
final int result = readVarintBytes(inputStream);
if (result != RESULT_CONTINUE) {
return result;
}
elementId = (int) parseTempByteArray(varintBytesLength, false);
elementIdState = STATE_FINISHED_READING;
return RESULT_CONTINUE;
}
/**
* Reads an element's content size such that reading can be stopped and started again in a later
* call if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if an entire element
* size has been read into {@link #elementContentSize}. Reset {@link #elementContentSizeState} to
* {@link #STATE_BEGIN_READING} before calling to indicate a new element size should be read.
*
* @param inputStream The input stream from which an element size should be read.
* @return One of the {@code RESULT_*} flags defined in this class.
*/
private int readElementContentSize(NonBlockingInputStream inputStream) {
if (elementContentSizeState == STATE_FINISHED_READING) {
return RESULT_CONTINUE;
}
if (elementContentSizeState == STATE_BEGIN_READING) {
varintBytesState = STATE_BEGIN_READING;
elementContentSizeState = STATE_READ_CONTENTS;
}
final int result = readVarintBytes(inputStream);
if (result != RESULT_CONTINUE) {
return result;
}
elementContentSize = parseTempByteArray(varintBytesLength, true);
elementContentSizeState = STATE_FINISHED_READING;
return RESULT_CONTINUE;
}
/**
* Reads an EBML variable-length integer (varint) such that reading can be stopped and started
* again in a later call if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if
* an entire varint has been read into {@link #tempByteArray} and the length of the varint is in
* {@link #varintBytesLength}. Reset {@link #varintBytesState} to {@link #STATE_BEGIN_READING}
* before calling to indicate a new varint should be read.
*
* @param inputStream The input stream from which a varint should be read.
* @return One of the {@code RESULT_*} flags defined in this class.
*/
private int readVarintBytes(NonBlockingInputStream inputStream) {
if (varintBytesState == STATE_FINISHED_READING) {
return RESULT_CONTINUE;
}
// Read first byte to get length.
if (varintBytesState == STATE_BEGIN_READING) {
bytesState = 0;
final int result = readBytes(inputStream, null, tempByteArray, 1);
if (result != RESULT_CONTINUE) {
return result;
}
varintBytesState = STATE_READ_CONTENTS;
final int firstByte = tempByteArray[0] & 0xff;
varintBytesLength = -1;
for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
varintBytesLength = i + 1;
break;
}
}
if (varintBytesLength == -1) {
throw new IllegalStateException(
"No valid varint length mask found at bytesRead = " + bytesRead);
}
}
// Read remaining bytes.
final int result = readBytes(inputStream, null, tempByteArray, varintBytesLength);
if (result != RESULT_CONTINUE) {
return result;
}
// All bytes have been read.
return RESULT_CONTINUE;
}
/**
* Reads a set amount of bytes into a {@link ByteBuffer}, {@code byte[]}, or nowhere (skipping
* the bytes) such that reading can be stopped and started again later if not enough bytes are
* available. Returns {@link #RESULT_CONTINUE} if all bytes have been read. Reset
* {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
*
* <p>If both {@code byteBuffer} and {@code byteArray} are not null then bytes are only read
* into {@code byteBuffer}.
*
* @param inputStream The input stream from which bytes should be read.
* @param byteBuffer The optional {@link ByteBuffer} into which bytes should be read.
* @param byteArray The optional {@code byte[]} into which bytes should be read.
* @param totalBytes The total size of bytes to be read or skipped.
* @return One of the {@code RESULT_*} flags defined in this class.
*/
private int readBytes(
NonBlockingInputStream inputStream, ByteBuffer byteBuffer, byte[] byteArray, int totalBytes) {
if (bytesState == STATE_BEGIN_READING
&& ((byteBuffer != null && totalBytes > byteBuffer.capacity())
|| (byteArray != null && totalBytes > byteArray.length))) {
throw new IllegalStateException("Byte destination not large enough");
}
if (bytesState < totalBytes) {
final int remainingBytes = totalBytes - bytesState;
final int result;
if (byteBuffer != null) {
result = inputStream.read(byteBuffer, remainingBytes);
} else if (byteArray != null) {
result = inputStream.read(byteArray, bytesState, remainingBytes);
} else {
result = inputStream.skip(remainingBytes);
}
if (result == -1) {
return RESULT_END_OF_FILE;
}
bytesState += result;
bytesRead += result;
if (bytesState < totalBytes) {
return RESULT_NEED_MORE_DATA;
}
}
return RESULT_CONTINUE;
}
/**
* Parses and returns the integer value currently read into the first {@code byteLength} bytes
* of {@link #tempByteArray}. EBML varint length masks can optionally be removed.
*
* @param byteLength The number of bytes to parse from {@link #tempByteArray}.
* @param removeLengthMask Removes the variable-length integer length mask from the value.
* @return The resulting integer value. This value could be up to 8-bytes so a Java long is used.
*/
private long parseTempByteArray(int byteLength, boolean removeLengthMask) {
if (removeLengthMask) {
tempByteArray[0] &= ~VARINT_LENGTH_MASKS[varintBytesLength - 1];
}
long varint = 0;
for (int i = 0; i < byteLength; i++) {
// Shift all existing bits up one byte and add the next byte at the bottom.
varint = (varint << 8) | (tempByteArray[i] & 0xff);
}
return varint;
}
/**
* Used in {@link #masterElementsStack} to track when the current master element ends so that
* {@link #onMasterElementEnd(int)} is called.
*/
private static final class MasterElement {
private final int elementId;
private final long elementEndOffset;
private MasterElement(int elementId, long elementEndOffset) {
this.elementId = elementId;
this.elementEndOffset = elementEndOffset;
}
}
public void skipBytes(NonBlockingInputStream inputStream, int totalBytes);
}

View File

@ -19,97 +19,22 @@ import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.parser.SegmentIndex;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.LongArray;
import com.google.android.exoplayer.util.MimeTypes;
import android.annotation.TargetApi;
import android.media.MediaExtractor;
import java.util.Arrays;
/**
* Facilitates the extraction of data from the WebM container format with a
* non-blocking, incremental parser based on {@link EbmlReader}.
* Extractor to facilitate data retrieval from the WebM container format.
*
* <p>WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
* More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>.
*/
@TargetApi(16)
public final class WebmExtractor extends EbmlReader {
private static final String DOC_TYPE_WEBM = "webm";
private static final String CODEC_ID_VP9 = "V_VP9";
private static final int UNKNOWN = -1;
// Element IDs
private static final int ID_EBML = 0x1A45DFA3;
private static final int ID_EBML_READ_VERSION = 0x42F7;
private static final int ID_DOC_TYPE = 0x4282;
private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
private static final int ID_SEGMENT = 0x18538067;
private static final int ID_INFO = 0x1549A966;
private static final int ID_TIMECODE_SCALE = 0x2AD7B1;
private static final int ID_DURATION = 0x4489;
private static final int ID_CLUSTER = 0x1F43B675;
private static final int ID_TIME_CODE = 0xE7;
private static final int ID_SIMPLE_BLOCK = 0xA3;
private static final int ID_TRACKS = 0x1654AE6B;
private static final int ID_TRACK_ENTRY = 0xAE;
private static final int ID_CODEC_ID = 0x86;
private static final int ID_VIDEO = 0xE0;
private static final int ID_PIXEL_WIDTH = 0xB0;
private static final int ID_PIXEL_HEIGHT = 0xBA;
private static final int ID_CUES = 0x1C53BB6B;
private static final int ID_CUE_POINT = 0xBB;
private static final int ID_CUE_TIME = 0xB3;
private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
// SimpleBlock Lacing Values
private static final int LACING_NONE = 0;
private static final int LACING_XIPH = 1;
private static final int LACING_FIXED = 2;
private static final int LACING_EBML = 3;
private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
private SampleHolder tempSampleHolder;
private boolean sampleRead;
private boolean prepared = false;
private long segmentStartPosition = UNKNOWN;
private long segmentEndPosition = UNKNOWN;
private long timecodeScale = 1000000L;
private long durationUs = UNKNOWN;
private int pixelWidth = UNKNOWN;
private int pixelHeight = UNKNOWN;
private int cuesByteSize = UNKNOWN;
private long clusterTimecodeUs = UNKNOWN;
private long simpleBlockTimecodeUs = UNKNOWN;
private MediaFormat format;
private SegmentIndex cues;
private LongArray cueTimesUs;
private LongArray cueClusterPositions;
public WebmExtractor() {
cueTimesUs = new LongArray();
cueClusterPositions = new LongArray();
}
public interface WebmExtractor {
/**
* Whether the has parsed the cues and sample format from the stream.
*
* @return True if the extractor is prepared. False otherwise.
* @return True if the extractor is prepared. False otherwise
*/
public boolean isPrepared() {
return prepared;
}
public boolean isPrepared();
/**
* Consumes data from a {@link NonBlockingInputStream}.
@ -118,289 +43,36 @@ public final class WebmExtractor extends EbmlReader {
* {@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 sampleHolder A {@link SampleHolder} into which the sample should be read.
* @return {@code true} if a sample has been read into the sample holder, otherwise {@code false}.
* @param inputStream The input stream from which data 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
*/
public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
tempSampleHolder = sampleHolder;
sampleRead = false;
super.read(inputStream);
tempSampleHolder = null;
return sampleRead;
}
public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder);
/**
* Seeks to a position before or equal to the requested time.
*
* @param seekTimeUs The desired seek time in microseconds.
* @param seekTimeUs The desired seek time in microseconds
* @param allowNoop Allow the seek operation to do nothing if the seek time is in the current
* segment, is equal to or greater than the time of the current sample, and if there does not
* exist a sync frame between these two times.
* @return True if the operation resulted in a change of state. False if it was a no-op.
* exist a sync frame between these two times
* @return True if the operation resulted in a change of state. False if it was a no-op
*/
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
checkPrepared();
if (allowNoop && simpleBlockTimecodeUs != UNKNOWN && seekTimeUs >= simpleBlockTimecodeUs) {
final int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) {
return false;
}
}
reset();
return true;
}
public boolean seekTo(long seekTimeUs, boolean allowNoop);
/**
* Returns the cues for the media stream.
*
* @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet
* prepared.
* prepared
*/
public SegmentIndex getCues() {
checkPrepared();
return cues;
}
public SegmentIndex getCues();
/**
* Returns the format of the samples contained within the media stream.
*
* @return The sample media format, or null if the extracted is not yet prepared.
* @return The sample media format, or null if the extracted is not yet prepared
*/
public MediaFormat getFormat() {
checkPrepared();
return format;
}
@Override
protected int getElementType(int id) {
switch (id) {
case ID_EBML:
case ID_SEGMENT:
case ID_INFO:
case ID_CLUSTER:
case ID_TRACKS:
case ID_TRACK_ENTRY:
case ID_VIDEO:
case ID_CUES:
case ID_CUE_POINT:
case ID_CUE_TRACK_POSITIONS:
return EbmlReader.TYPE_MASTER;
case ID_EBML_READ_VERSION:
case ID_DOC_TYPE_READ_VERSION:
case ID_TIMECODE_SCALE:
case ID_TIME_CODE:
case ID_PIXEL_WIDTH:
case ID_PIXEL_HEIGHT:
case ID_CUE_TIME:
case ID_CUE_CLUSTER_POSITION:
return EbmlReader.TYPE_UNSIGNED_INT;
case ID_DOC_TYPE:
case ID_CODEC_ID:
return EbmlReader.TYPE_STRING;
case ID_SIMPLE_BLOCK:
return EbmlReader.TYPE_BINARY;
case ID_DURATION:
return EbmlReader.TYPE_FLOAT;
default:
return EbmlReader.TYPE_UNKNOWN;
}
}
@Override
protected boolean onMasterElementStart(
int id, long elementOffset, int headerSize, int contentsSize) {
switch (id) {
case ID_SEGMENT:
if (segmentStartPosition != UNKNOWN || segmentEndPosition != UNKNOWN) {
throw new IllegalStateException("Multiple Segment elements not supported");
}
segmentStartPosition = elementOffset + headerSize;
segmentEndPosition = elementOffset + headerSize + contentsSize;
break;
case ID_CUES:
cuesByteSize = headerSize + contentsSize;
break;
}
return true;
}
@Override
protected boolean onMasterElementEnd(int id) {
switch (id) {
case ID_CUES:
finishPreparing();
return false;
}
return true;
}
@Override
protected boolean onIntegerElement(int id, long value) {
switch (id) {
case ID_EBML_READ_VERSION:
// Validate that EBMLReadVersion is supported. This extractor only supports v1.
if (value != 1) {
throw new IllegalStateException("EBMLReadVersion " + value + " not supported");
}
break;
case ID_DOC_TYPE_READ_VERSION:
// Validate that DocTypeReadVersion is supported. This extractor only supports up to v2.
if (value < 1 || value > 2) {
throw new IllegalStateException("DocTypeReadVersion " + value + " not supported");
}
break;
case ID_TIMECODE_SCALE:
timecodeScale = value;
break;
case ID_PIXEL_WIDTH:
pixelWidth = (int) value;
break;
case ID_PIXEL_HEIGHT:
pixelHeight = (int) value;
break;
case ID_CUE_TIME:
cueTimesUs.add(scaleTimecodeToUs(value));
break;
case ID_CUE_CLUSTER_POSITION:
cueClusterPositions.add(value);
break;
case ID_TIME_CODE:
clusterTimecodeUs = scaleTimecodeToUs(value);
break;
}
return true;
}
@Override
protected boolean onFloatElement(int id, double value) {
switch (id) {
case ID_DURATION:
durationUs = scaleTimecodeToUs(value);
break;
}
return true;
}
@Override
protected boolean onStringElement(int id, String value) {
switch (id) {
case ID_DOC_TYPE:
// Validate that DocType is supported. This extractor only supports "webm".
if (!DOC_TYPE_WEBM.equals(value)) {
throw new IllegalStateException("DocType " + value + " not supported");
}
break;
case ID_CODEC_ID:
// Validate that CodecID is supported. This extractor only supports "V_VP9".
if (!CODEC_ID_VP9.equals(value)) {
throw new IllegalStateException("CodecID " + value + " not supported");
}
break;
}
return true;
}
@Override
protected boolean onBinaryElement(NonBlockingInputStream inputStream,
int id, long elementOffset, int headerSize, int contentsSize) {
switch (id) {
case ID_SIMPLE_BLOCK:
// Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
// for info about how data is organized in a SimpleBlock element.
// Value of trackNumber is not used but needs to be read.
readVarint(inputStream);
// Next three bytes have timecode and flags.
readBytes(inputStream, simpleBlockTimecodeAndFlags, 3);
// First two bytes of the three are the relative timecode.
final int timecode =
(simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff);
final long timecodeUs = scaleTimecodeToUs(timecode);
// Last byte of the three has some flags and the lacing value.
final boolean keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80;
final boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
final int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
//final boolean discardable = (simpleBlockTimecodeAndFlags[2] & 0x01) == 0x01; // Not used.
// Validate lacing and set info into sample holder.
switch (lacing) {
case LACING_NONE:
final long elementEndOffset = elementOffset + headerSize + contentsSize;
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
tempSampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
tempSampleHolder.decodeOnly = invisible;
tempSampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
tempSampleHolder.size = (int) (elementEndOffset - getBytesRead());
break;
case LACING_EBML:
case LACING_FIXED:
case LACING_XIPH:
default:
throw new IllegalStateException("Lacing mode " + lacing + " not supported");
}
// Read video data into sample holder.
readBytes(inputStream, tempSampleHolder.data, tempSampleHolder.size);
sampleRead = true;
return false;
default:
skipBytes(inputStream, contentsSize);
}
return true;
}
private long scaleTimecodeToUs(long unscaledTimecode) {
return (unscaledTimecode * timecodeScale) / 1000L;
}
private long scaleTimecodeToUs(double unscaledTimecode) {
return (long) ((unscaledTimecode * timecodeScale) / 1000.0);
}
private void checkPrepared() {
if (!prepared) {
throw new IllegalStateException("Parser not yet prepared");
}
}
private void finishPreparing() {
if (prepared
|| segmentStartPosition == UNKNOWN || segmentEndPosition == UNKNOWN
|| durationUs == UNKNOWN
|| pixelWidth == UNKNOWN || pixelHeight == UNKNOWN
|| cuesByteSize == UNKNOWN
|| cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
throw new IllegalStateException("Incorrect state in finishPreparing()");
}
format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth,
pixelHeight, null);
final int cuePointsSize = cueTimesUs.size();
final int sizeBytes = cuesByteSize;
final int[] sizes = new int[cuePointsSize];
final long[] offsets = new long[cuePointsSize];
final long[] durationsUs = new long[cuePointsSize];
final long[] timesUs = new long[cuePointsSize];
for (int i = 0; i < cuePointsSize; i++) {
timesUs[i] = cueTimesUs.get(i);
offsets[i] = segmentStartPosition + cueClusterPositions.get(i);
}
for (int i = 0; i < cuePointsSize - 1; i++) {
sizes[i] = (int) (offsets[i + 1] - offsets[i]);
durationsUs[i] = timesUs[i + 1] - timesUs[i];
}
sizes[cuePointsSize - 1] = (int) (segmentEndPosition - offsets[cuePointsSize - 1]);
durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
cues = new SegmentIndex(sizeBytes, sizes, offsets, durationsUs, timesUs);
cueTimesUs = null;
cueClusterPositions = null;
prepared = true;
}
public MediaFormat getFormat();
}