Refactor WebM extractor.
This commit is contained in:
parent
9e16dec2f8
commit
686ac2a6f5
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user