diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java index 57714f380e..24173d3617 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java @@ -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; diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java new file mode 100644 index 0000000000..f66b83293f --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java @@ -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. + * + *

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 here. + */ +/* 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. + * + *

{@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 masterElementsStack = new Stack(); + + /** + * 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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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; + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java new file mode 100644 index 0000000000..b0b0936fe1 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java @@ -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. + * + *

WebM is a subset of the EBML elements defined for Matroska. More information about EBML and + * Matroska is available here. + * More info about WebM is here. + */ +@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; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java new file mode 100644 index 0000000000..1ddb51c589 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlEventHandler.java @@ -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. + * + *

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}. + * + *

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}. + * + *

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}. + * + *

Several methods in {@link EbmlReader} are available for reading the contents of a + * binary element: + *