Commit 4a1fed9e by Oliver Woodman

Add new style WebM extractor.

parent 6c5af232
Showing with 2799 additions and 7 deletions
/*
* 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.extractor;
import com.google.android.exoplayer.util.Util;
/**
* Defines chunks of samples within a media stream.
*/
public final class ChunkIndex implements SeekMap {
/**
* The number of chunks.
*/
public final int length;
/**
* The chunk sizes, in bytes.
*/
public final int[] sizes;
/**
* The chunk byte offsets.
*/
public final long[] offsets;
/**
* The chunk durations, in microseconds.
*/
public final long[] durationsUs;
/**
* The start time of each chunk, in microseconds.
*/
public final long[] timesUs;
/**
* @param sizes The chunk sizes, in bytes.
* @param offsets The chunk byte offsets.
* @param durationsUs The chunk durations, in microseconds.
* @param timesUs The start time of each chunk, in microseconds.
*/
public ChunkIndex(int[] sizes, long[] offsets, long[] durationsUs, long[] timesUs) {
this.length = sizes.length;
this.sizes = sizes;
this.offsets = offsets;
this.durationsUs = durationsUs;
this.timesUs = timesUs;
}
/**
* Obtains the index of the chunk corresponding to a given time.
*
* @param timeUs The time, in microseconds.
* @return The index of the corresponding chunk.
*/
public int getChunkIndex(long timeUs) {
return Util.binarySearchFloor(timesUs, timeUs, true, true);
}
@Override
public long getPosition(long timeUs) {
return offsets[getChunkIndex(timeUs)];
}
}
...@@ -213,6 +213,11 @@ public final class DefaultTrackOutput implements TrackOutput { ...@@ -213,6 +213,11 @@ public final class DefaultTrackOutput implements TrackOutput {
} }
@Override @Override
public int sampleData(ExtractorInput input, int length) throws IOException, InterruptedException {
return rollingBuffer.appendData(input, length);
}
@Override
public void sampleData(ParsableByteArray buffer, int length) { public void sampleData(ParsableByteArray buffer, int length) {
rollingBuffer.appendData(buffer, length); rollingBuffer.appendData(buffer, length);
} }
......
...@@ -55,4 +55,14 @@ public interface Extractor { ...@@ -55,4 +55,14 @@ public interface Extractor {
*/ */
int read(ExtractorInput input) throws IOException, InterruptedException; int read(ExtractorInput input) throws IOException, InterruptedException;
/**
* Notifies the extractor that a seek has occurred.
* <p>
* Following a call to this method, the {@link ExtractorInput} passed to the next invocation of
* {@link #read(ExtractorInput)} is required to provide data starting from any random access
* position in the stream. Random access positions can be obtained from a {@link SeekMap} that
* has been extracted and passed to the {@link ExtractorOutput}.
*/
void seek();
} }
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer.extractor; package com.google.android.exoplayer.extractor;
import com.google.android.exoplayer.drm.DrmInitData;
/** /**
* Receives stream level data extracted by an {@link Extractor}. * Receives stream level data extracted by an {@link Extractor}.
*/ */
...@@ -36,4 +38,18 @@ public interface ExtractorOutput { ...@@ -36,4 +38,18 @@ public interface ExtractorOutput {
*/ */
void endTracks(); void endTracks();
/**
* Invoked when a {@link SeekMap} has been extracted from the stream.
*
* @param seekMap The extracted {@link SeekMap}.
*/
void seekMap(SeekMap seekMap);
/**
* Invoked when {@link DrmInitData} has been extracted from the stream.
*
* @param drmInitData The extracted {@link DrmInitData}.
*/
void drmInitData(DrmInitData drmInitData);
} }
...@@ -315,6 +315,23 @@ import java.util.concurrent.ConcurrentLinkedQueue; ...@@ -315,6 +315,23 @@ import java.util.concurrent.ConcurrentLinkedQueue;
/** /**
* Appends data to the rolling buffer. * Appends data to the rolling buffer.
* *
* @param input The source from which to read.
* @param length The maximum length of the read.
* @return The number of bytes appended.
* @throws IOException If an error occurs reading from the source.
*/
public int appendData(ExtractorInput input, int length) throws IOException, InterruptedException {
ensureSpaceForWrite();
int thisWriteLength = Math.min(length, fragmentLength - lastFragmentOffset);
input.readFully(lastFragment, lastFragmentOffset, thisWriteLength);
lastFragmentOffset += thisWriteLength;
totalBytesWritten += thisWriteLength;
return thisWriteLength;
}
/**
* Appends data to the rolling buffer.
*
* @param buffer A buffer containing the data to append. * @param buffer A buffer containing the data to append.
* @param length The length of the data to append. * @param length The length of the data to append.
*/ */
......
/*
* 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.extractor;
/**
* Maps seek positions (in microseconds) to corresponding positions (byte offsets) in the stream.
*/
public interface SeekMap {
/**
* Maps a seek position in microseconds to a corresponding position (byte offset) in the stream
* from which data can be provided to the extractor.
*
* @param timeUs A seek position in microseconds.
* @return The corresponding position (byte offset) in the stream from which data can be provided
* to the extractor.
*/
long getPosition(long timeUs);
}
...@@ -19,6 +19,8 @@ import com.google.android.exoplayer.MediaFormat; ...@@ -19,6 +19,8 @@ import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/** /**
* Receives track level data extracted by an {@link Extractor}. * Receives track level data extracted by an {@link Extractor}.
*/ */
...@@ -34,6 +36,17 @@ public interface TrackOutput { ...@@ -34,6 +36,17 @@ public interface TrackOutput {
/** /**
* Invoked to write sample data to the output. * Invoked to write sample data to the output.
* *
* @param input An {@link ExtractorInput} from which to read the sample data.
* @param length The maximum length to read from the input.
* @return The number of bytes appended.
* @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted.
*/
int sampleData(ExtractorInput input, int length) throws IOException, InterruptedException;
/**
* Invoked to write sample data to the output.
*
* @param data A {@link ParsableByteArray} from which to read the sample data. * @param data A {@link ParsableByteArray} from which to read the sample data.
* @param length The number of bytes to read. * @param length The number of bytes to read.
*/ */
...@@ -43,14 +56,14 @@ public interface TrackOutput { ...@@ -43,14 +56,14 @@ public interface TrackOutput {
* Invoked when metadata associated with a sample has been extracted from the stream. * Invoked when metadata associated with a sample has been extracted from the stream.
* <p> * <p>
* The corresponding sample data will have already been passed to the output via calls to * The corresponding sample data will have already been passed to the output via calls to
* {@link #sampleData(ParsableByteArray, int)}. * {@link #sampleData(ExtractorInput, int)} or {@link #sampleData(ParsableByteArray, int)}.
* *
* @param timeUs The media timestamp associated with the sample, in microseconds. * @param timeUs The media timestamp associated with the sample, in microseconds.
* @param flags Flags associated with the sample. See {@link SampleHolder#flags}. * @param flags Flags associated with the sample. See {@link SampleHolder#flags}.
* @param size The size of the sample data, in bytes. * @param size The size of the sample data, in bytes.
* @param offset The number of bytes that have been passed to * @param offset The number of bytes that have been passed to
* {@link #sampleData(ParsableByteArray, int)} since the last byte belonging to the sample * {@link #sampleData(ExtractorInput, int)} or {@link #sampleData(ParsableByteArray, int)}
* whose metadata is being passed. * since the last byte belonging to the sample whose metadata is being passed.
* @param encryptionKey The encryption key associated with the sample. May be null. * @param encryptionKey The encryption key associated with the sample. May be null.
*/ */
void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey); void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey);
......
...@@ -50,8 +50,12 @@ public class AdtsExtractor implements Extractor { ...@@ -50,8 +50,12 @@ public class AdtsExtractor implements Extractor {
} }
@Override @Override
public int read(ExtractorInput input) public void seek() {
throws IOException, InterruptedException { throw new UnsupportedOperationException();
}
@Override
public int read(ExtractorInput input) throws IOException, InterruptedException {
int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE); int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
if (bytesRead == -1) { if (bytesRead == -1) {
return RESULT_END_OF_INPUT; return RESULT_END_OF_INPUT;
......
...@@ -72,8 +72,12 @@ public final class TsExtractor implements Extractor { ...@@ -72,8 +72,12 @@ public final class TsExtractor implements Extractor {
} }
@Override @Override
public int read(ExtractorInput input) public void seek() {
throws IOException, InterruptedException { throw new UnsupportedOperationException();
}
@Override
public int read(ExtractorInput input) throws IOException, InterruptedException {
if (!input.readFully(tsPacketBuffer.data, 0, TS_PACKET_SIZE, true)) { if (!input.readFully(tsPacketBuffer.data, 0, TS_PACKET_SIZE, true)) {
return RESULT_END_OF_INPUT; return RESULT_END_OF_INPUT;
} }
......
/*
* 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.extractor.webm;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.util.Assertions;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Stack;
/**
* Default implementation of {@link EbmlReader}.
*/
/* package */ final class DefaultEbmlReader implements EbmlReader {
private static final int ELEMENT_STATE_READ_ID = 0;
private static final int ELEMENT_STATE_READ_CONTENT_SIZE = 1;
private static final int ELEMENT_STATE_READ_CONTENT = 2;
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;
private final byte[] scratch = new byte[8];
private final Stack<MasterElement> masterElementsStack = new Stack<MasterElement>();
private final VarintReader varintReader = new VarintReader();
private EbmlReaderOutput output;
private int elementState;
private int elementId;
private long elementContentSize;
@Override
public void init(EbmlReaderOutput eventHandler) {
this.output = eventHandler;
}
@Override
public void reset() {
elementState = ELEMENT_STATE_READ_ID;
masterElementsStack.clear();
varintReader.reset();
}
@Override
public boolean read(ExtractorInput input) throws IOException, InterruptedException {
Assertions.checkState(output != null);
while (true) {
if (!masterElementsStack.isEmpty()
&& input.getPosition() >= masterElementsStack.peek().elementEndPosition) {
output.endMasterElement(masterElementsStack.pop().elementId);
return true;
}
if (elementState == ELEMENT_STATE_READ_ID) {
long result = varintReader.readUnsignedVarint(input, true, false);
if (result == -1) {
return false;
}
// Element IDs are at most 4 bytes, so we can cast to integers.
elementId = (int) result;
elementState = ELEMENT_STATE_READ_CONTENT_SIZE;
}
if (elementState == ELEMENT_STATE_READ_CONTENT_SIZE) {
elementContentSize = varintReader.readUnsignedVarint(input, false, true);
elementState = ELEMENT_STATE_READ_CONTENT;
}
int type = output.getElementType(elementId);
switch (type) {
case TYPE_MASTER:
long elementContentPosition = input.getPosition();
long elementEndPosition = elementContentPosition + elementContentSize;
masterElementsStack.add(new MasterElement(elementId, elementEndPosition));
output.startMasterElement(elementId, elementContentPosition, elementContentSize);
elementState = ELEMENT_STATE_READ_ID;
return true;
case TYPE_UNSIGNED_INT:
if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
throw new IllegalStateException("Invalid integer size: " + elementContentSize);
}
output.integerElement(elementId, readInteger(input, (int) elementContentSize));
elementState = ELEMENT_STATE_READ_ID;
return true;
case TYPE_FLOAT:
if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
&& elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
throw new IllegalStateException("Invalid float size: " + elementContentSize);
}
output.floatElement(elementId, readFloat(input, (int) elementContentSize));
elementState = ELEMENT_STATE_READ_ID;
return true;
case TYPE_STRING:
if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException("String element size: " + elementContentSize);
}
output.stringElement(elementId, readString(input, (int) elementContentSize));
elementState = ELEMENT_STATE_READ_ID;
return true;
case TYPE_BINARY:
output.binaryElement(elementId, (int) elementContentSize, input);
elementState = ELEMENT_STATE_READ_ID;
return true;
case TYPE_UNKNOWN:
input.skipFully((int) elementContentSize);
elementState = ELEMENT_STATE_READ_ID;
break;
default:
throw new IllegalStateException("Invalid element type " + type);
}
}
}
/**
* Reads and returns an integer of length {@code byteLength} from the {@link ExtractorInput}.
*
* @param input The {@link ExtractorInput} from which to read.
* @param byteLength The length of the integer being read.
* @return The read integer value.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
private long readInteger(ExtractorInput input, int byteLength)
throws IOException, InterruptedException {
input.readFully(scratch, 0, byteLength);
long value = 0;
for (int i = 0; i < byteLength; i++) {
value = (value << 8) | (scratch[i] & 0xFF);
}
return value;
}
/**
* Reads and returns a float of length {@code byteLength} from the {@link ExtractorInput}.
*
* @param input The {@link ExtractorInput} from which to read.
* @param byteLength The length of the float being read.
* @return The read float value.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
private double readFloat(ExtractorInput input, int byteLength)
throws IOException, InterruptedException {
long integerValue = readInteger(input, byteLength);
double floatValue;
if (byteLength == VALID_FLOAT32_ELEMENT_SIZE_BYTES) {
floatValue = Float.intBitsToFloat((int) integerValue);
} else {
floatValue = Double.longBitsToDouble(integerValue);
}
return floatValue;
}
/**
* Reads and returns a string of length {@code byteLength} from the {@link ExtractorInput}.
*
* @param input The {@link ExtractorInput} from which to read.
* @param byteLength The length of the float being read.
* @return The read string value.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
private String readString(ExtractorInput input, int byteLength)
throws IOException, InterruptedException {
byte[] stringBytes = new byte[byteLength];
input.readFully(stringBytes, 0, byteLength);
return new String(stringBytes, Charset.forName(C.UTF8_NAME));
}
/**
* Used in {@link #masterElementsStack} to track when the current master element ends, so that
* {@link EbmlReaderOutput#endMasterElement(int)} can be called.
*/
private static final class MasterElement {
private final int elementId;
private final long elementEndPosition;
private MasterElement(int elementId, long elementEndPosition) {
this.elementId = elementId;
this.elementEndPosition = elementEndPosition;
}
}
}
/*
* 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.extractor.webm;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.ExtractorInput;
import java.io.IOException;
/**
* Event-driven EBML reader that delivers events to an {@link EbmlReaderOutput}.
* <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 */ interface EbmlReader {
/**
* Type for unknown elements.
*/
public static final int TYPE_UNKNOWN = 0;
/**
* Type for elements that contain child elements.
*/
public static final int TYPE_MASTER = 1;
/**
* Type for integer value elements of up to 8 bytes.
*/
public static final int TYPE_UNSIGNED_INT = 2;
/**
* Type for string elements.
*/
public static final int TYPE_STRING = 3;
/**
* Type for binary elements.
*/
public static final int TYPE_BINARY = 4;
/**
* Type for IEEE floating point value elements of either 4 or 8 bytes.
*/
public static final int TYPE_FLOAT = 5;
/**
* Initializes the extractor with an {@link EbmlReaderOutput}.
*
* @param output An {@link EbmlReaderOutput} to receive events.
*/
public void init(EbmlReaderOutput output);
/**
* Resets the state of the reader.
* <p>
* Subsequent calls to {@link #read(ExtractorInput)} will start reading a new EBML structure
* from scratch.
*/
public void reset();
/**
* Reads from an {@link ExtractorInput}, invoking an event callback if possible.
*
* @param input The {@link ExtractorInput} from which data should be read.
* @return True if data can continue to be read. False if the end of the input was encountered.
* @throws ParserException If parsing fails.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
public boolean read(ExtractorInput input) throws ParserException, IOException,
InterruptedException;
}
/*
* 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.extractor.webm;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.ExtractorInput;
import java.io.IOException;
/**
* Defines EBML element IDs/types and reacts to events.
*/
/* package */ interface EbmlReaderOutput {
/**
* Maps an element ID to a corresponding type.
* <p>
* If {@link EbmlReader#TYPE_UNKNOWN} is returned then the element is skipped. Note that all
* children of a skipped element are also skipped.
*
* @param id The element ID to map.
* @return One of the {@code TYPE_} constants defined in {@link EbmlReader}.
*/
int getElementType(int id);
/**
* Called when the start of a master element is encountered.
* <p>
* Following events should be considered as taking place within this element until a matching call
* to {@link #endMasterElement(int)} is made.
* <p>
* Note that it is possible for another master element of the same element ID to be nested within
* itself.
*
* @param id The element ID.
* @param contentPosition The position of the start of the element's content in the stream.
* @param contentSize The size of the element's content in bytes.
* @throws ParserException If a parsing error occurs.
*/
void startMasterElement(int id, long contentPosition, long contentSize) throws ParserException;
/**
* Called when the end of a master element is encountered.
*
* @param id The element ID.
* @throws ParserException If a parsing error occurs.
*/
void endMasterElement(int id) throws ParserException;
/**
* Called when an integer element is encountered.
*
* @param id The element ID.
* @param value The integer value that the element contains.
* @throws ParserException If a parsing error occurs.
*/
void integerElement(int id, long value) throws ParserException;
/**
* Called when a float element is encountered.
*
* @param id The element ID.
* @param value The float value that the element contains
* @throws ParserException If a parsing error occurs.
*/
void floatElement(int id, double value) throws ParserException;
/**
* Called when a string element is encountered.
*
* @param id The element ID.
* @param value The string value that the element contains.
* @throws ParserException If a parsing error occurs.
*/
void stringElement(int id, String value) throws ParserException;
/**
* Called when a binary element is encountered.
* <p>
* The element header (containing the element ID and content size) will already have been read.
* Implementations are required to consume the whole remainder of the element, which is
* {@code contentSize} bytes in length, before returning. Implementations are permitted to fail
* (by throwing an exception) having partially consumed the data, however if they do this, they
* must consume the remainder of the content when invoked again.
*
* @param id The element ID.
* @param contentsSize The element's content size.
* @param input The {@link ExtractorInput} from which data should be read.
* @throws ParserException If a parsing error occurs.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
void binaryElement(int id, int contentsSize, ExtractorInput input)
throws ParserException, IOException, InterruptedException;
}
package com.google.android.exoplayer.extractor.webm;
import com.google.android.exoplayer.extractor.ExtractorInput;
import java.io.EOFException;
import java.io.IOException;
/**
* Reads EBML variable-length integers (varints) from an {@link ExtractorInput}.
*/
/* package */ class VarintReader {
private static final int STATE_BEGIN_READING = 0;
private static final int STATE_READ_CONTENTS = 1;
/**
* 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 final byte[] scratch;
private int state;
private int length;
public VarintReader() {
scratch = new byte[8];
}
/**
* Resets the reader to start reading a new variable-length integer.
*/
public void reset() {
state = STATE_BEGIN_READING;
length = 0;
}
/**
* Reads an EBML variable-length integer (varint) from an {@link ExtractorInput} such that
* reading can be resumed later if an error occurs having read only some of it.
* <p>
* If an value is successfully read, then the reader will automatically reset itself ready to
* read another value.
* <p>
* If an {@link IOException} or {@link InterruptedException} is throw, the read can be resumed
* later by calling this method again, passing an {@link ExtractorInput} providing data starting
* where the previous one left off.
*
* @param input The {@link ExtractorInput} from which the integer should be read.
* @param allowEndOfInput True if encountering the end of the input having read no data is
* allowed, and should result in {@code -1} being returned. False if it should be
* considered an error, causing an {@link EOFException} to be thrown.
* @param removeLengthMask Removes the variable-length integer length mask from the value
* @return The read value, or -1 if {@code allowEndOfStream} is true and the end of the input was
* encountered.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
public long readUnsignedVarint(ExtractorInput input, boolean allowEndOfInput,
boolean removeLengthMask) throws IOException, InterruptedException {
if (state == STATE_BEGIN_READING) {
// Read the first byte to establish the length.
if (!input.readFully(scratch, 0, 1, allowEndOfInput)) {
return -1;
}
int firstByte = scratch[0] & 0xFF;
length = -1;
for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
length = i + 1;
break;
}
}
if (length == -1) {
throw new IllegalStateException("No valid varint length mask found");
}
state = STATE_READ_CONTENTS;
}
// Read the remaining bytes.
input.readFully(scratch, 1, length - 1);
// Parse the value.
if (removeLengthMask) {
scratch[0] &= ~VARINT_LENGTH_MASKS[length - 1];
}
long varint = 0;
for (int i = 0; i < length; i++) {
varint = (varint << 8) | (scratch[i] & 0xFF);
}
state = STATE_BEGIN_READING;
return varint;
}
/**
* Returns the number of bytes occupied by the most recently parsed varint.
*/
public int getLastLength() {
return length;
}
}
/*
* 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.extractor.webm;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.extractor.ChunkIndex;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.util.LongArray;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* 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>.
* RFC on encrypted WebM can be found
* <a href="http://wiki.webmproject.org/encryption/webm-encryption-rfc">here</a>.
*/
public final class WebmExtractor implements Extractor {
private static final int SAMPLE_STATE_START = 0;
private static final int SAMPLE_STATE_HEADER = 1;
private static final int SAMPLE_STATE_DATA = 2;
private static final String DOC_TYPE_WEBM = "webm";
private static final String CODEC_ID_VP9 = "V_VP9";
private static final String CODEC_ID_VORBIS = "A_VORBIS";
private static final String CODEC_ID_OPUS = "A_OPUS";
private static final int VORBIS_MAX_INPUT_SIZE = 8192;
private static final int OPUS_MAX_INPUT_SIZE = 5760;
private static final int ENCRYPTION_IV_SIZE = 8;
private static final int UNKNOWN = -1;
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_BLOCK_GROUP = 0xA0;
private static final int ID_BLOCK = 0xA1;
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_CODEC_PRIVATE = 0x63A2;
private static final int ID_CODEC_DELAY = 0x56AA;
private static final int ID_SEEK_PRE_ROLL = 0x56BB;
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_AUDIO = 0xE1;
private static final int ID_CHANNELS = 0x9F;
private static final int ID_SAMPLING_FREQUENCY = 0xB5;
private static final int ID_CONTENT_ENCODINGS = 0x6D80;
private static final int ID_CONTENT_ENCODING = 0x6240;
private static final int ID_CONTENT_ENCODING_ORDER = 0x5031;
private static final int ID_CONTENT_ENCODING_SCOPE = 0x5032;
private static final int ID_CONTENT_ENCODING_TYPE = 0x5033;
private static final int ID_CONTENT_ENCRYPTION = 0x5035;
private static final int ID_CONTENT_ENCRYPTION_ALGORITHM = 0x47E1;
private static final int ID_CONTENT_ENCRYPTION_KEY_ID = 0x47E2;
private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS = 0x47E7;
private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE = 0x47E8;
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;
private static final int LACING_NONE = 0;
private final EbmlReader reader;
private final VarintReader varintReader;
private final ParsableByteArray sampleHeaderScratch;
private long segmentContentPosition = UNKNOWN;
private long segmentContentSize = UNKNOWN;
private long timecodeScale = 1000000L;
private long durationUs = C.UNKNOWN_TIME_US;
private int pixelWidth = UNKNOWN;
private int pixelHeight = UNKNOWN;
private int channelCount = UNKNOWN;
private int sampleRate = UNKNOWN;
private byte[] codecPrivate;
private String codecId;
private long codecDelayNs;
private long seekPreRollNs;
private boolean isAudioTrack;
private boolean hasContentEncryption;
private byte[] encryptionKeyId;
private long clusterTimecodeUs = UNKNOWN;
private LongArray cueTimesUs;
private LongArray cueClusterPositions;
// Sample reading state.
private int blockBytesRead;
private int sampleState;
private int sampleSize;
private int sampleFlags;
private long sampleTimeUs;
private boolean sampleRead;
// Extractor outputs.
private ExtractorOutput extractorOutput;
private TrackOutput trackOutput;
public WebmExtractor() {
this(new DefaultEbmlReader());
}
/* package */ WebmExtractor(EbmlReader reader) {
this.reader = reader;
this.reader.init(new InnerEbmlReaderOutput());
varintReader = new VarintReader();
sampleHeaderScratch = new ParsableByteArray(4);
}
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
trackOutput = output.track(0);
extractorOutput.endTracks();
}
@Override
public void seek() {
clusterTimecodeUs = UNKNOWN;
sampleState = SAMPLE_STATE_START;
reader.reset();
varintReader.reset();
}
@Override
public int read(ExtractorInput input) throws IOException, InterruptedException {
sampleRead = false;
boolean inputHasData = true;
while (!sampleRead && inputHasData) {
inputHasData = reader.read(input);
}
return inputHasData ? Extractor.RESULT_CONTINUE : Extractor.RESULT_END_OF_INPUT;
}
/* package */ 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_AUDIO:
case ID_VIDEO:
case ID_CONTENT_ENCODINGS:
case ID_CONTENT_ENCODING:
case ID_CONTENT_ENCRYPTION:
case ID_CONTENT_ENCRYPTION_AES_SETTINGS:
case ID_CUES:
case ID_CUE_POINT:
case ID_CUE_TRACK_POSITIONS:
case ID_BLOCK_GROUP:
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_CODEC_DELAY:
case ID_SEEK_PRE_ROLL:
case ID_CHANNELS:
case ID_CONTENT_ENCODING_ORDER:
case ID_CONTENT_ENCODING_SCOPE:
case ID_CONTENT_ENCODING_TYPE:
case ID_CONTENT_ENCRYPTION_ALGORITHM:
case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:
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_CONTENT_ENCRYPTION_KEY_ID:
case ID_SIMPLE_BLOCK:
case ID_BLOCK:
case ID_CODEC_PRIVATE:
return EbmlReader.TYPE_BINARY;
case ID_DURATION:
case ID_SAMPLING_FREQUENCY:
return EbmlReader.TYPE_FLOAT;
default:
return EbmlReader.TYPE_UNKNOWN;
}
}
/* package */ void startMasterElement(int id, long contentPosition, long contentSize)
throws ParserException {
switch (id) {
case ID_SEGMENT:
if (segmentContentPosition != UNKNOWN) {
throw new ParserException("Multiple Segment elements not supported");
}
segmentContentPosition = contentPosition;
segmentContentSize = contentSize;
return;
case ID_CUES:
cueTimesUs = new LongArray();
cueClusterPositions = new LongArray();
return;
case ID_CONTENT_ENCODING:
// TODO: check and fail if more than one content encoding is present.
return;
case ID_CONTENT_ENCRYPTION:
hasContentEncryption = true;
return;
default:
return;
}
}
/* package */ void endMasterElement(int id) throws ParserException {
switch (id) {
case ID_CUES:
extractorOutput.seekMap(buildCues());
return;
case ID_CONTENT_ENCODING:
if (!hasContentEncryption) {
// We found a ContentEncoding other than Encryption.
throw new ParserException("Found an unsupported ContentEncoding");
}
if (encryptionKeyId == null) {
throw new ParserException("Encrypted Track found but ContentEncKeyID was not found");
}
extractorOutput.drmInitData(
new DrmInitData.Universal(MimeTypes.VIDEO_WEBM, encryptionKeyId));
return;
case ID_AUDIO:
isAudioTrack = true;
return;
case ID_TRACK_ENTRY:
trackOutput.format(isAudioTrack ? buildAudioFormat() : buildVideoFormat());
return;
default:
return;
}
}
/* package */ void integerElement(int id, long value) throws ParserException {
switch (id) {
case ID_EBML_READ_VERSION:
// Validate that EBMLReadVersion is supported. This extractor only supports v1.
if (value != 1) {
throw new ParserException("EBMLReadVersion " + value + " not supported");
}
return;
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 ParserException("DocTypeReadVersion " + value + " not supported");
}
return;
case ID_TIMECODE_SCALE:
timecodeScale = value;
return;
case ID_PIXEL_WIDTH:
pixelWidth = (int) value;
return;
case ID_PIXEL_HEIGHT:
pixelHeight = (int) value;
return;
case ID_CODEC_DELAY:
codecDelayNs = value;
return;
case ID_SEEK_PRE_ROLL:
seekPreRollNs = value;
return;
case ID_CHANNELS:
channelCount = (int) value;
return;
case ID_CONTENT_ENCODING_ORDER:
// This extractor only supports one ContentEncoding element and hence the order has to be 0.
if (value != 0) {
throw new ParserException("ContentEncodingOrder " + value + " not supported");
}
return;
case ID_CONTENT_ENCODING_SCOPE:
// This extractor only supports the scope of all frames (since that's the only scope used
// for Encryption).
if (value != 1) {
throw new ParserException("ContentEncodingScope " + value + " not supported");
}
return;
case ID_CONTENT_ENCODING_TYPE:
// This extractor only supports Encrypted ContentEncodingType.
if (value != 1) {
throw new ParserException("ContentEncodingType " + value + " not supported");
}
return;
case ID_CONTENT_ENCRYPTION_ALGORITHM:
// Only the value 5 (AES) is allowed according to the WebM specification.
if (value != 5) {
throw new ParserException("ContentEncAlgo " + value + " not supported");
}
return;
case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:
// Only the value 1 is allowed according to the WebM specification.
if (value != 1) {
throw new ParserException("AESSettingsCipherMode " + value + " not supported");
}
return;
case ID_CUE_TIME:
cueTimesUs.add(scaleTimecodeToUs(value));
return;
case ID_CUE_CLUSTER_POSITION:
cueClusterPositions.add(value);
return;
case ID_TIME_CODE:
clusterTimecodeUs = scaleTimecodeToUs(value);
return;
default:
return;
}
}
/* package */ void floatElement(int id, double value) {
switch (id) {
case ID_DURATION:
durationUs = scaleTimecodeToUs((long) value);
return;
case ID_SAMPLING_FREQUENCY:
sampleRate = (int) value;
return;
default:
return;
}
}
/* package */ void stringElement(int id, String value) throws ParserException {
switch (id) {
case ID_DOC_TYPE:
// Validate that DocType is supported. This extractor only supports "webm".
if (!DOC_TYPE_WEBM.equals(value)) {
throw new ParserException("DocType " + value + " not supported");
}
return;
case ID_CODEC_ID:
// Validate that CodecID is supported.
if (!isCodecSupported(value)) {
throw new ParserException("CodecID " + value + " not supported");
}
codecId = value;
return;
default:
return;
}
}
/* package */ void binaryElement(int id, int contentSize, ExtractorInput input)
throws IOException, InterruptedException {
switch (id) {
case ID_CODEC_PRIVATE:
codecPrivate = new byte[contentSize];
input.readFully(codecPrivate, 0, contentSize);
return;
case ID_CONTENT_ENCRYPTION_KEY_ID:
encryptionKeyId = new byte[contentSize];
input.readFully(encryptionKeyId, 0, contentSize);
return;
case ID_SIMPLE_BLOCK:
case ID_BLOCK:
// Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
// and http://matroska.org/technical/specs/index.html#block_structure
// for info about how data is organized in SimpleBlock and Block elements respectively. They
// differ only in the way flags are specified.
if (sampleState == SAMPLE_STATE_START) {
// Value of trackNumber is not used but needs to be read.
varintReader.readUnsignedVarint(input, false, false);
blockBytesRead = varintReader.getLastLength();
sampleState = SAMPLE_STATE_HEADER;
}
if (sampleState == SAMPLE_STATE_HEADER) {
byte[] sampleHeaderScratchData = sampleHeaderScratch.data;
// Next 3 bytes have timecode and flags. If encrypted, the 4th byte is a signal byte.
int remainingHeaderLength = hasContentEncryption ? 4 : 3;
input.readFully(sampleHeaderScratchData, 0, remainingHeaderLength);
blockBytesRead += remainingHeaderLength;
// First two bytes are the relative timecode.
int timecode = (sampleHeaderScratchData[0] << 8)
| (sampleHeaderScratchData[1] & 0xFF);
sampleTimeUs = clusterTimecodeUs + scaleTimecodeToUs(timecode);
// Third byte contains the lacing value and some flags.
int lacing = (sampleHeaderScratchData[2] & 0x06) >> 1;
if (lacing != LACING_NONE) {
throw new ParserException("Lacing mode not supported: " + lacing);
}
boolean isInvisible = (sampleHeaderScratchData[2] & 0x08) == 0x08;
boolean isKeyframe;
if (id == ID_BLOCK) {
// Matroska Block element does not self-sufficiently say whether it is a keyframe. It
// depends on the existence of another element (ReferenceBlock) which may occur after
// the Block element. Since this extractor uses Block element only for Opus, we set the
// keyframe to be true always since all Opus frames are key frames.
isKeyframe = true;
} else {
isKeyframe = (sampleHeaderScratchData[2] & 0x80) == 0x80;
}
boolean isEncrypted = false;
// If encrypted, the fourth byte is an encryption signal byte.
if (hasContentEncryption) {
if ((sampleHeaderScratchData[3] & 0x80) == 0x80) {
throw new ParserException("Extension bit is set in signal byte");
}
isEncrypted = (sampleHeaderScratchData[3] & 0x01) == 0x01;
}
sampleFlags = (isKeyframe ? C.SAMPLE_FLAG_SYNC : 0)
| (isInvisible ? C.SAMPLE_FLAG_DECODE_ONLY : 0)
| (isEncrypted ? C.SAMPLE_FLAG_ENCRYPTED : 0);
sampleSize = contentSize - blockBytesRead;
if (isEncrypted) {
// Write the vector size.
sampleHeaderScratch.data[0] = (byte) ENCRYPTION_IV_SIZE;
sampleHeaderScratch.setPosition(0);
trackOutput.sampleData(sampleHeaderScratch, 1);
sampleSize++;
}
sampleState = SAMPLE_STATE_DATA;
}
while (blockBytesRead < contentSize) {
blockBytesRead += trackOutput.sampleData(input, contentSize - blockBytesRead);
}
trackOutput.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, null);
sampleState = SAMPLE_STATE_START;
sampleRead = true;
return;
default:
throw new IllegalStateException("Unexpected id: " + id);
}
}
/**
* Builds an video {@link MediaFormat} containing recently gathered Audio information.
*
* @return The built {@link MediaFormat}.
* @throws ParserException If the codec is unsupported.
*/
private MediaFormat buildVideoFormat() throws ParserException {
if (CODEC_ID_VP9.equals(codecId)) {
return MediaFormat.createVideoFormat(MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, durationUs,
pixelWidth, pixelHeight, null);
} else {
throw new ParserException("Unable to build format");
}
}
/**
* Builds an audio {@link MediaFormat} containing recently gathered Audio information.
*
* @return The built {@link MediaFormat}.
* @throws ParserException If the codec is unsupported.
*/
private MediaFormat buildAudioFormat() throws ParserException {
if (CODEC_ID_VORBIS.equals(codecId)) {
return MediaFormat.createAudioFormat(MimeTypes.AUDIO_VORBIS, VORBIS_MAX_INPUT_SIZE,
durationUs, channelCount, sampleRate, parseVorbisCodecPrivate());
} else if (CODEC_ID_OPUS.equals(codecId)) {
ArrayList<byte[]> opusInitializationData = new ArrayList<byte[]>(3);
opusInitializationData.add(codecPrivate);
opusInitializationData.add(ByteBuffer.allocate(Long.SIZE).putLong(codecDelayNs).array());
opusInitializationData.add(ByteBuffer.allocate(Long.SIZE).putLong(seekPreRollNs).array());
return MediaFormat.createAudioFormat(MimeTypes.AUDIO_OPUS, OPUS_MAX_INPUT_SIZE,
durationUs, channelCount, sampleRate, opusInitializationData);
} else {
throw new ParserException("Unable to build format");
}
}
/**
* Builds a {@link ChunkIndex} containing recently gathered Cues information.
*
* @return The built {@link ChunkIndex}.
* @throws ParserException If the index could not be built.
*/
private ChunkIndex buildCues() throws ParserException {
if (segmentContentPosition == UNKNOWN) {
throw new ParserException("Segment start/end offsets unknown");
} else if (durationUs == C.UNKNOWN_TIME_US) {
throw new ParserException("Duration unknown");
} else if (cueTimesUs == null || cueClusterPositions == null
|| cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
throw new ParserException("Invalid/missing cue points");
}
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] = segmentContentPosition + 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) (segmentContentPosition + segmentContentSize - offsets[cuePointsSize - 1]);
durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
cueTimesUs = null;
cueClusterPositions = null;
return new ChunkIndex(sizes, offsets, durationsUs, timesUs);
}
/**
* Builds initialization data for a {@link MediaFormat} from Vorbis codec private data.
*
* @return The initialization data for the {@link MediaFormat}.
* @throws ParserException If the initialization data could not be built.
*/
private ArrayList<byte[]> parseVorbisCodecPrivate() throws ParserException {
try {
if (codecPrivate[0] != 0x02) {
throw new ParserException("Error parsing vorbis codec private");
}
int offset = 1;
int vorbisInfoLength = 0;
while (codecPrivate[offset] == (byte) 0xFF) {
vorbisInfoLength += 0xFF;
offset++;
}
vorbisInfoLength += codecPrivate[offset++];
int vorbisSkipLength = 0;
while (codecPrivate[offset] == (byte) 0xFF) {
vorbisSkipLength += 0xFF;
offset++;
}
vorbisSkipLength += codecPrivate[offset++];
if (codecPrivate[offset] != 0x01) {
throw new ParserException("Error parsing vorbis codec private");
}
byte[] vorbisInfo = new byte[vorbisInfoLength];
System.arraycopy(codecPrivate, offset, vorbisInfo, 0, vorbisInfoLength);
offset += vorbisInfoLength;
if (codecPrivate[offset] != 0x03) {
throw new ParserException("Error parsing vorbis codec private");
}
offset += vorbisSkipLength;
if (codecPrivate[offset] != 0x05) {
throw new ParserException("Error parsing vorbis codec private");
}
byte[] vorbisBooks = new byte[codecPrivate.length - offset];
System.arraycopy(codecPrivate, offset, vorbisBooks, 0, codecPrivate.length - offset);
ArrayList<byte[]> initializationData = new ArrayList<byte[]>(2);
initializationData.add(vorbisInfo);
initializationData.add(vorbisBooks);
return initializationData;
} catch (ArrayIndexOutOfBoundsException e) {
throw new ParserException("Error parsing vorbis codec private");
}
}
private long scaleTimecodeToUs(long unscaledTimecode) {
return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
}
private boolean isCodecSupported(String codecId) {
return CODEC_ID_VP9.equals(codecId)
|| CODEC_ID_OPUS.equals(codecId)
|| CODEC_ID_VORBIS.equals(codecId);
}
/**
* Passes events through to the outer {@link WebmExtractor}.
*/
private final class InnerEbmlReaderOutput implements EbmlReaderOutput {
@Override
public int getElementType(int id) {
return WebmExtractor.this.getElementType(id);
}
@Override
public void startMasterElement(int id, long contentPosition, long contentSize)
throws ParserException {
WebmExtractor.this.startMasterElement(id, contentPosition, contentSize);
}
@Override
public void endMasterElement(int id) throws ParserException {
WebmExtractor.this.endMasterElement(id);
}
@Override
public void integerElement(int id, long value) throws ParserException {
WebmExtractor.this.integerElement(id, value);
}
@Override
public void floatElement(int id, double value) {
WebmExtractor.this.floatElement(id, value);
}
@Override
public void stringElement(int id, String value) throws ParserException {
WebmExtractor.this.stringElement(id, value);
}
@Override
public void binaryElement(int id, int contentsSize, ExtractorInput input)
throws IOException, InterruptedException {
WebmExtractor.this.binaryElement(id, contentsSize, input);
}
}
}
...@@ -17,10 +17,12 @@ package com.google.android.exoplayer.hls; ...@@ -17,10 +17,12 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.extractor.DefaultTrackOutput; import com.google.android.exoplayer.extractor.DefaultTrackOutput;
import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput; import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
...@@ -209,4 +211,14 @@ public final class HlsExtractorWrapper implements ExtractorOutput { ...@@ -209,4 +211,14 @@ public final class HlsExtractorWrapper implements ExtractorOutput {
this.tracksBuilt = true; this.tracksBuilt = true;
} }
@Override
public void seekMap(SeekMap seekMap) {
// Do nothing.
}
@Override
public void drmInitData(DrmInitData drmInit) {
// Do nothing.
}
} }
/*
* 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.extractor.webm;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.extractor.DefaultExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.testutil.FakeDataSource;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import android.net.Uri;
import junit.framework.TestCase;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Tests {@link DefaultEbmlReader}.
*/
public class DefaultEbmlReaderTest extends TestCase {
public void testMasterElement() throws IOException, InterruptedException {
ExtractorInput input = createTestInput(0x1A, 0x45, 0xDF, 0xA3, 0x84, 0x42, 0x85, 0x81, 0x01);
TestOutput expected = new TestOutput();
expected.startMasterElement(TestOutput.ID_EBML, 5, 4);
expected.integerElement(TestOutput.ID_DOC_TYPE_READ_VERSION, 1);
expected.endMasterElement(TestOutput.ID_EBML);
assertEvents(input, expected.events);
}
public void testMasterElementEmpty() throws IOException, InterruptedException {
ExtractorInput input = createTestInput(0x18, 0x53, 0x80, 0x67, 0x80);
TestOutput expected = new TestOutput();
expected.startMasterElement(TestOutput.ID_SEGMENT, 5, 0);
expected.endMasterElement(TestOutput.ID_SEGMENT);
assertEvents(input, expected.events);
}
public void testUnsignedIntegerElement() throws IOException, InterruptedException {
// 0xFE is chosen because for signed integers it should be interpreted as -2
ExtractorInput input = createTestInput(0x42, 0xF7, 0x81, 0xFE);
TestOutput expected = new TestOutput();
expected.integerElement(TestOutput.ID_EBML_READ_VERSION, 254);
assertEvents(input, expected.events);
}
public void testUnsignedIntegerElementLarge() throws IOException, InterruptedException {
ExtractorInput input =
createTestInput(0x42, 0xF7, 0x88, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF);
TestOutput expected = new TestOutput();
expected.integerElement(TestOutput.ID_EBML_READ_VERSION, Long.MAX_VALUE);
assertEvents(input, expected.events);
}
public void testUnsignedIntegerElementTooLargeBecomesNegative()
throws IOException, InterruptedException {
ExtractorInput input =
createTestInput(0x42, 0xF7, 0x88, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF);
TestOutput expected = new TestOutput();
expected.integerElement(TestOutput.ID_EBML_READ_VERSION, -1);
assertEvents(input, expected.events);
}
public void testStringElement() throws IOException, InterruptedException {
ExtractorInput input = createTestInput(0x42, 0x82, 0x86, 0x41, 0x62, 0x63, 0x31, 0x32, 0x33);
TestOutput expected = new TestOutput();
expected.stringElement(TestOutput.ID_DOC_TYPE, "Abc123");
assertEvents(input, expected.events);
}
public void testStringElementEmpty() throws IOException, InterruptedException {
ExtractorInput input = createTestInput(0x42, 0x82, 0x80);
TestOutput expected = new TestOutput();
expected.stringElement(TestOutput.ID_DOC_TYPE, "");
assertEvents(input, expected.events);
}
public void testFloatElementFourBytes() throws IOException, InterruptedException {
ExtractorInput input =
createTestInput(0x44, 0x89, 0x84, 0x3F, 0x80, 0x00, 0x00);
TestOutput expected = new TestOutput();
expected.floatElement(TestOutput.ID_DURATION, 1.0);
assertEvents(input, expected.events);
}
public void testFloatElementEightBytes() throws IOException, InterruptedException {
ExtractorInput input =
createTestInput(0x44, 0x89, 0x88, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
TestOutput expected = new TestOutput();
expected.floatElement(TestOutput.ID_DURATION, -2.0);
assertEvents(input, expected.events);
}
public void testBinaryElement() throws IOException, InterruptedException {
ExtractorInput input =
createTestInput(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08);
TestOutput expected = new TestOutput();
expected.binaryElement(TestOutput.ID_SIMPLE_BLOCK, 8,
createTestInput(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08));
assertEvents(input, expected.events);
}
private static void assertEvents(ExtractorInput input, List<String> expectedEvents)
throws IOException, InterruptedException {
DefaultEbmlReader reader = new DefaultEbmlReader();
TestOutput output = new TestOutput();
reader.init(output);
// We expect the number of successful reads to equal the number of expected events.
for (int i = 0; i < expectedEvents.size(); i++) {
assertTrue(reader.read(input));
}
// The next read should be unsuccessful.
assertFalse(reader.read(input));
// Check that we really did get to the end of input.
assertFalse(input.readFully(new byte[1], 0, 1, true));
assertEquals(expectedEvents.size(), output.events.size());
for (int i = 0; i < expectedEvents.size(); i++) {
assertEquals(expectedEvents.get(i), output.events.get(i));
}
}
/**
* Helper to build an {@link ExtractorInput} from byte data.
* <p>
* Each argument must be able to cast to a byte value.
*
* @param data Zero or more integers with values between {@code 0x00} and {@code 0xFF}.
* @return An {@link ExtractorInput} from which the data can be read.
* @throws IOException If an error occurs creating the input.
*/
private static ExtractorInput createTestInput(int... data) throws IOException {
byte[] bytes = new byte[data.length];
for (int i = 0; i < data.length; i++) {
bytes[i] = (byte) data[i];
}
DataSource dataSource = new FakeDataSource.Builder().appendReadData(bytes).build();
dataSource.open(new DataSpec(Uri.parse("http://www.google.com")));
ExtractorInput input = new DefaultExtractorInput(dataSource, 0, C.LENGTH_UNBOUNDED);
return input;
}
/**
* An {@link EbmlReaderOutput} that records each event callback.
*/
private static final class TestOutput implements EbmlReaderOutput {
// 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_DURATION = 0x4489;
private static final int ID_SIMPLE_BLOCK = 0xA3;
private final List<String> events = new ArrayList<String>();
@Override
public int getElementType(int id) {
switch (id) {
case ID_EBML:
case ID_SEGMENT:
return EbmlReader.TYPE_MASTER;
case ID_EBML_READ_VERSION:
case ID_DOC_TYPE_READ_VERSION:
return EbmlReader.TYPE_UNSIGNED_INT;
case ID_DOC_TYPE:
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 void startMasterElement(int id, long contentPosition, long contentSize) {
events.add(formatEvent(id, "start contentPosition=" + contentPosition
+ " contentSize=" + contentSize));
}
@Override
public void endMasterElement(int id) {
events.add(formatEvent(id, "end"));
}
@Override
public void integerElement(int id, long value) {
events.add(formatEvent(id, "integer=" + String.valueOf(value)));
}
@Override
public void floatElement(int id, double value) {
events.add(formatEvent(id, "float=" + String.valueOf(value)));
}
@Override
public void stringElement(int id, String value) {
events.add(formatEvent(id, "string=" + value));
}
@Override
public void binaryElement(int id, int contentSize, ExtractorInput input)
throws IOException, InterruptedException {
byte[] bytes = new byte[contentSize];
input.readFully(bytes, 0, contentSize);
events.add(formatEvent(id, "bytes=" + Arrays.toString(bytes)));
}
private static String formatEvent(int id, String event) {
return "[" + Integer.toHexString(id) + "] " + event;
}
}
}
/*
* 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.extractor.webm;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.extractor.DefaultExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.testutil.FakeDataSource;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import android.net.Uri;
import junit.framework.TestCase;
import java.io.EOFException;
import java.io.IOException;
import java.util.Arrays;
/**
* Tests for {@link VarintReader}.
*/
public class VarintReaderTest extends TestCase {
private static final String TEST_URI = "http://www.google.com";
private static final byte MAX_BYTE = (byte) 0xFF;
private static final byte[] DATA_1_BYTE_0 = new byte[] {(byte) 0x80};
private static final byte[] DATA_2_BYTE_0 = new byte[] {0x40, 0};
private static final byte[] DATA_3_BYTE_0 = new byte[] {0x20, 0, 0};
private static final byte[] DATA_4_BYTE_0 = new byte[] {0x10, 0, 0, 0};
private static final byte[] DATA_5_BYTE_0 = new byte[] {0x08, 0, 0, 0, 0};
private static final byte[] DATA_6_BYTE_0 = new byte[] {0x04, 0, 0, 0, 0, 0};
private static final byte[] DATA_7_BYTE_0 = new byte[] {0x02, 0, 0, 0, 0, 0, 0};
private static final byte[] DATA_8_BYTE_0 = new byte[] {0x01, 0, 0, 0, 0, 0, 0, 0};
private static final byte[] DATA_1_BYTE_64 = new byte[] {(byte) 0xC0};
private static final byte[] DATA_2_BYTE_64 = new byte[] {0x40, 0x40};
private static final byte[] DATA_3_BYTE_64 = new byte[] {0x20, 0, 0x40};
private static final byte[] DATA_4_BYTE_64 = new byte[] {0x10, 0, 0, 0x40};
private static final byte[] DATA_5_BYTE_64 = new byte[] {0x08, 0, 0, 0, 0x40};
private static final byte[] DATA_6_BYTE_64 = new byte[] {0x04, 0, 0, 0, 0, 0x40};
private static final byte[] DATA_7_BYTE_64 = new byte[] {0x02, 0, 0, 0, 0, 0, 0x40};
private static final byte[] DATA_8_BYTE_64 = new byte[] {0x01, 0, 0, 0, 0, 0, 0, 0x40};
private static final byte[] DATA_1_BYTE_MAX = new byte[] {MAX_BYTE};
private static final byte[] DATA_2_BYTE_MAX = new byte[] {0x7F, MAX_BYTE};
private static final byte[] DATA_3_BYTE_MAX = new byte[] {0x3F, MAX_BYTE, MAX_BYTE};
private static final byte[] DATA_4_BYTE_MAX = new byte[] {0x1F, MAX_BYTE, MAX_BYTE, MAX_BYTE};
private static final byte[] DATA_5_BYTE_MAX =
new byte[] {0x0F, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE};
private static final byte[] DATA_6_BYTE_MAX =
new byte[] {0x07, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE};
private static final byte[] DATA_7_BYTE_MAX =
new byte[] {0x03, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE};
private static final byte[] DATA_8_BYTE_MAX =
new byte[] {0x01, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE};
private static final long VALUE_1_BYTE_MAX = 0x7F;
private static final long VALUE_1_BYTE_MAX_WITH_MASK = 0xFF;
private static final long VALUE_2_BYTE_MAX = 0x3FFF;
private static final long VALUE_2_BYTE_MAX_WITH_MASK = 0x7FFF;
private static final long VALUE_3_BYTE_MAX = 0x1FFFFF;
private static final long VALUE_3_BYTE_MAX_WITH_MASK = 0x3FFFFF;
private static final long VALUE_4_BYTE_MAX = 0xFFFFFFF;
private static final long VALUE_4_BYTE_MAX_WITH_MASK = 0x1FFFFFFF;
private static final long VALUE_5_BYTE_MAX = 0x7FFFFFFFFL;
private static final long VALUE_5_BYTE_MAX_WITH_MASK = 0xFFFFFFFFFL;
private static final long VALUE_6_BYTE_MAX = 0x3FFFFFFFFFFL;
private static final long VALUE_6_BYTE_MAX_WITH_MASK = 0x7FFFFFFFFFFL;
private static final long VALUE_7_BYTE_MAX = 0x1FFFFFFFFFFFFL;
private static final long VALUE_7_BYTE_MAX_WITH_MASK = 0x3FFFFFFFFFFFFL;
private static final long VALUE_8_BYTE_MAX = 0xFFFFFFFFFFFFFFL;
private static final long VALUE_8_BYTE_MAX_WITH_MASK = 0x1FFFFFFFFFFFFFFL;
public void testReadVarintEndOfInputAtStart() throws IOException, InterruptedException {
VarintReader reader = new VarintReader();
// Build an input, and read to the end.
DataSource dataSource = buildDataSource(new byte[1]);
dataSource.open(new DataSpec(Uri.parse(TEST_URI)));
ExtractorInput input = new DefaultExtractorInput(dataSource, 0, C.LENGTH_UNBOUNDED);
int bytesRead = input.read(new byte[1], 0, 1);
assertEquals(1, bytesRead);
// End of input allowed.
long result = reader.readUnsignedVarint(input, true, false);
assertEquals(-1, result);
// End of input not allowed.
try {
reader.readUnsignedVarint(input, false, false);
fail();
} catch (EOFException e) {
// Expected.
}
}
public void testReadVarint() throws IOException, InterruptedException {
VarintReader reader = new VarintReader();
testReadVarint(reader, true, DATA_1_BYTE_0, 1, 0);
testReadVarint(reader, true, DATA_2_BYTE_0, 2, 0);
testReadVarint(reader, true, DATA_3_BYTE_0, 3, 0);
testReadVarint(reader, true, DATA_4_BYTE_0, 4, 0);
testReadVarint(reader, true, DATA_5_BYTE_0, 5, 0);
testReadVarint(reader, true, DATA_6_BYTE_0, 6, 0);
testReadVarint(reader, true, DATA_7_BYTE_0, 7, 0);
testReadVarint(reader, true, DATA_8_BYTE_0, 8, 0);
testReadVarint(reader, true, DATA_1_BYTE_64, 1, 64);
testReadVarint(reader, true, DATA_2_BYTE_64, 2, 64);
testReadVarint(reader, true, DATA_3_BYTE_64, 3, 64);
testReadVarint(reader, true, DATA_4_BYTE_64, 4, 64);
testReadVarint(reader, true, DATA_5_BYTE_64, 5, 64);
testReadVarint(reader, true, DATA_6_BYTE_64, 6, 64);
testReadVarint(reader, true, DATA_7_BYTE_64, 7, 64);
testReadVarint(reader, true, DATA_8_BYTE_64, 8, 64);
testReadVarint(reader, true, DATA_1_BYTE_MAX, 1, VALUE_1_BYTE_MAX);
testReadVarint(reader, true, DATA_2_BYTE_MAX, 2, VALUE_2_BYTE_MAX);
testReadVarint(reader, true, DATA_3_BYTE_MAX, 3, VALUE_3_BYTE_MAX);
testReadVarint(reader, true, DATA_4_BYTE_MAX, 4, VALUE_4_BYTE_MAX);
testReadVarint(reader, true, DATA_5_BYTE_MAX, 5, VALUE_5_BYTE_MAX);
testReadVarint(reader, true, DATA_6_BYTE_MAX, 6, VALUE_6_BYTE_MAX);
testReadVarint(reader, true, DATA_7_BYTE_MAX, 7, VALUE_7_BYTE_MAX);
testReadVarint(reader, true, DATA_8_BYTE_MAX, 8, VALUE_8_BYTE_MAX);
testReadVarint(reader, false, DATA_1_BYTE_MAX, 1, VALUE_1_BYTE_MAX_WITH_MASK);
testReadVarint(reader, false, DATA_2_BYTE_MAX, 2, VALUE_2_BYTE_MAX_WITH_MASK);
testReadVarint(reader, false, DATA_3_BYTE_MAX, 3, VALUE_3_BYTE_MAX_WITH_MASK);
testReadVarint(reader, false, DATA_4_BYTE_MAX, 4, VALUE_4_BYTE_MAX_WITH_MASK);
testReadVarint(reader, false, DATA_5_BYTE_MAX, 5, VALUE_5_BYTE_MAX_WITH_MASK);
testReadVarint(reader, false, DATA_6_BYTE_MAX, 6, VALUE_6_BYTE_MAX_WITH_MASK);
testReadVarint(reader, false, DATA_7_BYTE_MAX, 7, VALUE_7_BYTE_MAX_WITH_MASK);
testReadVarint(reader, false, DATA_8_BYTE_MAX, 8, VALUE_8_BYTE_MAX_WITH_MASK);
}
public void testReadVarintFlaky() throws IOException, InterruptedException {
VarintReader reader = new VarintReader();
testReadVarintFlaky(reader, true, DATA_1_BYTE_0, 1, 0);
testReadVarintFlaky(reader, true, DATA_2_BYTE_0, 2, 0);
testReadVarintFlaky(reader, true, DATA_3_BYTE_0, 3, 0);
testReadVarintFlaky(reader, true, DATA_4_BYTE_0, 4, 0);
testReadVarintFlaky(reader, true, DATA_5_BYTE_0, 5, 0);
testReadVarintFlaky(reader, true, DATA_6_BYTE_0, 6, 0);
testReadVarintFlaky(reader, true, DATA_7_BYTE_0, 7, 0);
testReadVarintFlaky(reader, true, DATA_8_BYTE_0, 8, 0);
testReadVarintFlaky(reader, true, DATA_1_BYTE_64, 1, 64);
testReadVarintFlaky(reader, true, DATA_2_BYTE_64, 2, 64);
testReadVarintFlaky(reader, true, DATA_3_BYTE_64, 3, 64);
testReadVarintFlaky(reader, true, DATA_4_BYTE_64, 4, 64);
testReadVarintFlaky(reader, true, DATA_5_BYTE_64, 5, 64);
testReadVarintFlaky(reader, true, DATA_6_BYTE_64, 6, 64);
testReadVarintFlaky(reader, true, DATA_7_BYTE_64, 7, 64);
testReadVarintFlaky(reader, true, DATA_8_BYTE_64, 8, 64);
testReadVarintFlaky(reader, true, DATA_1_BYTE_MAX, 1, VALUE_1_BYTE_MAX);
testReadVarintFlaky(reader, true, DATA_2_BYTE_MAX, 2, VALUE_2_BYTE_MAX);
testReadVarintFlaky(reader, true, DATA_3_BYTE_MAX, 3, VALUE_3_BYTE_MAX);
testReadVarintFlaky(reader, true, DATA_4_BYTE_MAX, 4, VALUE_4_BYTE_MAX);
testReadVarintFlaky(reader, true, DATA_5_BYTE_MAX, 5, VALUE_5_BYTE_MAX);
testReadVarintFlaky(reader, true, DATA_6_BYTE_MAX, 6, VALUE_6_BYTE_MAX);
testReadVarintFlaky(reader, true, DATA_7_BYTE_MAX, 7, VALUE_7_BYTE_MAX);
testReadVarintFlaky(reader, true, DATA_8_BYTE_MAX, 8, VALUE_8_BYTE_MAX);
testReadVarintFlaky(reader, false, DATA_1_BYTE_MAX, 1, VALUE_1_BYTE_MAX_WITH_MASK);
testReadVarintFlaky(reader, false, DATA_2_BYTE_MAX, 2, VALUE_2_BYTE_MAX_WITH_MASK);
testReadVarintFlaky(reader, false, DATA_3_BYTE_MAX, 3, VALUE_3_BYTE_MAX_WITH_MASK);
testReadVarintFlaky(reader, false, DATA_4_BYTE_MAX, 4, VALUE_4_BYTE_MAX_WITH_MASK);
testReadVarintFlaky(reader, false, DATA_5_BYTE_MAX, 5, VALUE_5_BYTE_MAX_WITH_MASK);
testReadVarintFlaky(reader, false, DATA_6_BYTE_MAX, 6, VALUE_6_BYTE_MAX_WITH_MASK);
testReadVarintFlaky(reader, false, DATA_7_BYTE_MAX, 7, VALUE_7_BYTE_MAX_WITH_MASK);
testReadVarintFlaky(reader, false, DATA_8_BYTE_MAX, 8, VALUE_8_BYTE_MAX_WITH_MASK);
}
private static void testReadVarint(VarintReader reader, boolean removeMask, byte[] data,
int expectedLength, long expectedValue) throws IOException, InterruptedException {
DataSource dataSource = buildDataSource(data);
dataSource.open(new DataSpec(Uri.parse(TEST_URI)));
ExtractorInput input = new DefaultExtractorInput(dataSource, 0, C.LENGTH_UNBOUNDED);
long result = reader.readUnsignedVarint(input, false, removeMask);
assertEquals(expectedLength, input.getPosition());
assertEquals(expectedValue, result);
}
private static void testReadVarintFlaky(VarintReader reader, boolean removeMask, byte[] data,
int expectedLength, long expectedValue) throws IOException, InterruptedException {
DataSource dataSource = buildFlakyDataSource(data);
ExtractorInput input = null;
long position = 0;
long result = -1;
while (result == -1) {
dataSource.open(new DataSpec(Uri.parse(TEST_URI), position, C.LENGTH_UNBOUNDED, null));
input = new DefaultExtractorInput(dataSource, position, C.LENGTH_UNBOUNDED);
try {
result = reader.readUnsignedVarint(input, false, removeMask);
position = input.getPosition();
} catch (IOException e) {
// Expected. We'll try again from the position that the input was advanced to.
position = input.getPosition();
dataSource.close();
}
}
assertEquals(expectedLength, input.getPosition());
assertEquals(expectedValue, result);
}
private static DataSource buildDataSource(byte[] data) {
FakeDataSource.Builder builder = new FakeDataSource.Builder();
builder.appendReadData(data);
return builder.build();
}
private static DataSource buildFlakyDataSource(byte[] data) {
FakeDataSource.Builder builder = new FakeDataSource.Builder();
builder.appendReadError(new IOException("A"));
builder.appendReadData(new byte[] {data[0]});
if (data.length > 1) {
builder.appendReadError(new IOException("B"));
builder.appendReadData(new byte[] {data[1]});
}
if (data.length > 2) {
builder.appendReadError(new IOException("C"));
builder.appendReadData(Arrays.copyOfRange(data, 2, data.length));
}
return builder.build();
}
}
/*
* 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.extractor.webm;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.extractor.ChunkIndex;
import com.google.android.exoplayer.extractor.DefaultExtractorInput;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.testutil.FakeDataSource;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import android.net.Uri;
import android.test.InstrumentationTestCase;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.UUID;
/**
* Tests for {@link WebmExtractor}.
*/
public class WebmExtractorTest extends InstrumentationTestCase {
private static final int CUE_POINT_ELEMENT_BYTE_SIZE = 31;
private static final int DEFAULT_TIMECODE_SCALE = 1000000;
private static final long TEST_DURATION_US = 9920000L;
private static final int TEST_WIDTH = 1280;
private static final int TEST_HEIGHT = 720;
private static final int TEST_CHANNEL_COUNT = 1;
private static final int TEST_SAMPLE_RATE = 48000;
private static final long TEST_CODEC_DELAY = 6500000;
private static final long TEST_SEEK_PRE_ROLL = 80000000;
private static final int TEST_OPUS_CODEC_PRIVATE_SIZE = 2;
private static final String TEST_VORBIS_CODEC_PRIVATE = "webm/vorbis_codec_private";
private static final int TEST_VORBIS_INFO_SIZE = 30;
private static final int TEST_VORBIS_BOOKS_SIZE = 4140;
private static final byte[] TEST_ENCRYPTION_KEY_ID = { 0x00, 0x01, 0x02, 0x03 };
private static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
private static final UUID ZERO_UUID = new UUID(0, 0);
private static final byte[] TEST_INITIALIZATION_VECTOR = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07
};
private static final int ID_VP9 = 0;
private static final int ID_OPUS = 1;
private static final int ID_VORBIS = 2;
private WebmExtractor extractor;
private TestOutput output;
@Override
public void setUp() {
extractor = new WebmExtractor();
output = new TestOutput();
extractor.init(output);
}
@Override
public void tearDown() {
extractor = null;
output = null;
}
public void testReadInitializationSegment() throws IOException, InterruptedException {
consume(createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null));
assertFormat();
assertIndex(new IndexPoint(0, 0, TEST_DURATION_US));
}
public void testPrepareOpus() throws IOException, InterruptedException {
consume(createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_OPUS, null));
assertAudioFormat(ID_OPUS);
assertIndex(new IndexPoint(0, 0, TEST_DURATION_US));
}
public void testPrepareVorbis() throws IOException, InterruptedException {
consume(createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VORBIS, null));
assertAudioFormat(ID_VORBIS);
assertIndex(new IndexPoint(0, 0, TEST_DURATION_US));
}
public void testPrepareContentEncodingEncryption() throws IOException, InterruptedException {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 1);
consume(createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
assertFormat();
assertIndex(new IndexPoint(0, 0, TEST_DURATION_US));
DrmInitData drmInitData = output.drmInitData;
assertNotNull(output.drmInitData);
android.test.MoreAsserts.assertEquals(TEST_ENCRYPTION_KEY_ID, drmInitData.get(WIDEVINE_UUID));
android.test.MoreAsserts.assertEquals(TEST_ENCRYPTION_KEY_ID, drmInitData.get(ZERO_UUID));
}
public void testPrepareThreeCuePoints() throws IOException, InterruptedException {
consume(createInitializationSegment(3, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null));
assertFormat();
assertIndex(
new IndexPoint(0, 0, 10000),
new IndexPoint(10000, 0, 10000),
new IndexPoint(20000, 0, TEST_DURATION_US - 20000));
}
public void testPrepareCustomTimecodeScale() throws IOException, InterruptedException {
consume(createInitializationSegment(3, 0, true, 1000, ID_VP9, null));
assertFormat();
assertIndex(
new IndexPoint(0, 0, 10),
new IndexPoint(10, 0, 10),
new IndexPoint(20, 0, (TEST_DURATION_US / 1000) - 20));
}
public void testPrepareNoCuePoints() throws IOException, InterruptedException {
try {
consume(createInitializationSegment(0, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null));
fail();
} catch (ParserException exception) {
assertEquals("Invalid/missing cue points", exception.getMessage());
}
}
public void testPrepareInvalidDocType() throws IOException, InterruptedException {
try {
consume(createInitializationSegment(1, 0, false, DEFAULT_TIMECODE_SCALE, ID_VP9, null));
fail();
} catch (ParserException exception) {
assertEquals("DocType webB not supported", exception.getMessage());
}
}
public void testPrepareInvalidContentEncodingOrder() throws IOException, InterruptedException {
ContentEncodingSettings settings = new ContentEncodingSettings(1, 1, 1, 5, 1);
try {
consume(createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
fail();
} catch (ParserException exception) {
assertEquals("ContentEncodingOrder 1 not supported", exception.getMessage());
}
}
public void testPrepareInvalidContentEncodingScope() throws IOException, InterruptedException {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 0, 1, 5, 1);
try {
consume(createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
fail();
} catch (ParserException exception) {
assertEquals("ContentEncodingScope 0 not supported", exception.getMessage());
}
}
public void testPrepareInvalidContentEncodingType() throws IOException, InterruptedException {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 0, 5, 1);
try {
consume(createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
fail();
} catch (ParserException exception) {
assertEquals("ContentEncodingType 0 not supported", exception.getMessage());
}
}
public void testPrepareInvalidContentEncAlgo() throws IOException, InterruptedException {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 4, 1);
try {
consume(createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
fail();
} catch (ParserException exception) {
assertEquals("ContentEncAlgo 4 not supported", exception.getMessage());
}
}
public void testPrepareInvalidAESSettingsCipherMode() throws IOException, InterruptedException {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 0);
try {
consume(createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
fail();
} catch (ParserException exception) {
assertEquals("AESSettingsCipherMode 0 not supported", exception.getMessage());
}
}
public void testReadSampleKeyframe() throws IOException, InterruptedException {
MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, true, false, false);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null),
mediaSegment.clusterBytes);
consume(testInputData);
assertFormat();
assertSample(mediaSegment, 0, true, false, false);
}
public void testReadBlock() throws IOException, InterruptedException {
MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, false, false, false);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_OPUS, null),
mediaSegment.clusterBytes);
consume(testInputData);
assertAudioFormat(ID_OPUS);
assertSample(mediaSegment, 0, true, false, false);
}
public void testReadEncryptedFrame() throws IOException, InterruptedException {
MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, true, true, true);
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 1);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings),
mediaSegment.clusterBytes);
consume(testInputData);
assertFormat();
assertSample(mediaSegment, 0, true, false, true);
}
public void testReadEncryptedFrameWithInvalidSignalByte()
throws IOException, InterruptedException {
MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, true, true, false);
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 1);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings),
mediaSegment.clusterBytes);
try {
consume(testInputData);
fail();
} catch (ParserException exception) {
assertEquals("Extension bit is set in signal byte", exception.getMessage());
}
}
public void testReadSampleInvisible() throws IOException, InterruptedException {
MediaSegment mediaSegment = createMediaSegment(100, 12, 13, false, true, true, false, false);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null),
mediaSegment.clusterBytes);
consume(testInputData);
assertFormat();
assertSample(mediaSegment, 25000, false, true, false);
}
public void testReadSampleCustomTimescale() throws IOException, InterruptedException {
MediaSegment mediaSegment = createMediaSegment(100, 12, 13, false, false, true, false, false);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, 1000, ID_VP9, null),
mediaSegment.clusterBytes);
consume(testInputData);
assertFormat();
assertSample(mediaSegment, 25, false, false, false);
}
public void testReadSampleNegativeSimpleBlockTimecode() throws IOException, InterruptedException {
MediaSegment mediaSegment = createMediaSegment(100, 13, -12, true, true, true, false, false);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null),
mediaSegment.clusterBytes);
consume(testInputData);
assertFormat();
assertSample(mediaSegment, 1000, true, true, false);
}
private void consume(byte[] data) throws IOException, InterruptedException {
ExtractorInput input = createTestInput(data);
int readResult = Extractor.RESULT_CONTINUE;
while (readResult == Extractor.RESULT_CONTINUE) {
readResult = extractor.read(input);
}
assertEquals(Extractor.RESULT_END_OF_INPUT, readResult);
}
private static ExtractorInput createTestInput(byte[] data) throws IOException {
DataSource dataSource = new FakeDataSource.Builder().appendReadData(data).build();
dataSource.open(new DataSpec(Uri.parse("http://www.google.com")));
ExtractorInput input = new DefaultExtractorInput(dataSource, 0, C.LENGTH_UNBOUNDED);
return input;
}
private void assertFormat() {
MediaFormat format = output.format;
assertEquals(TEST_WIDTH, format.width);
assertEquals(TEST_HEIGHT, format.height);
assertEquals(MimeTypes.VIDEO_VP9, format.mimeType);
}
private void assertAudioFormat(int codecId) {
MediaFormat format = output.format;
assertEquals(TEST_CHANNEL_COUNT, format.channelCount);
assertEquals(TEST_SAMPLE_RATE, format.sampleRate);
if (codecId == ID_OPUS) {
assertEquals(MimeTypes.AUDIO_OPUS, format.mimeType);
assertEquals(3, format.initializationData.size());
assertEquals(TEST_OPUS_CODEC_PRIVATE_SIZE, format.initializationData.get(0).length);
assertEquals(TEST_CODEC_DELAY, ByteBuffer.wrap(format.initializationData.get(1)).getLong());
assertEquals(TEST_SEEK_PRE_ROLL, ByteBuffer.wrap(format.initializationData.get(2)).getLong());
} else if (codecId == ID_VORBIS) {
assertEquals(MimeTypes.AUDIO_VORBIS, format.mimeType);
assertEquals(2, format.initializationData.size());
assertEquals(TEST_VORBIS_INFO_SIZE, format.initializationData.get(0).length);
assertEquals(TEST_VORBIS_BOOKS_SIZE, format.initializationData.get(1).length);
}
}
private void assertIndex(IndexPoint... indexPoints) {
ChunkIndex index = (ChunkIndex) output.seekMap;
assertEquals(indexPoints.length, index.length);
for (int i = 0; i < indexPoints.length; i++) {
IndexPoint indexPoint = indexPoints[i];
assertEquals(indexPoint.timeUs, index.timesUs[i]);
assertEquals(indexPoint.size, index.sizes[i]);
assertEquals(indexPoint.durationUs, index.durationsUs[i]);
}
}
private void assertSample(MediaSegment mediaSegment, int timeUs, boolean keyframe,
boolean invisible, boolean encrypted) {
byte[] expectedOutput = mediaSegment.videoBytes;
if (encrypted) {
expectedOutput = joinByteArrays(new byte[] {(byte) TEST_INITIALIZATION_VECTOR.length},
TEST_INITIALIZATION_VECTOR, expectedOutput);
}
assertTrue(Arrays.equals(expectedOutput, output.sampleData));
assertEquals(timeUs, output.sampleTimeUs);
assertEquals(keyframe, (output.sampleFlags & C.SAMPLE_FLAG_SYNC) != 0);
assertEquals(invisible, (output.sampleFlags & C.SAMPLE_FLAG_DECODE_ONLY) != 0);
assertEquals(encrypted, (output.sampleFlags & C.SAMPLE_FLAG_ENCRYPTED) != 0);
}
private byte[] createInitializationSegment(int cuePoints, int mediaSegmentSize,
boolean docTypeIsWebm, int timecodeScale, int codecId,
ContentEncodingSettings contentEncodingSettings) {
byte[] tracksElement = null;
switch (codecId) {
case ID_VP9:
tracksElement = createTracksElementWithVideo(
true, TEST_WIDTH, TEST_HEIGHT, contentEncodingSettings);
break;
case ID_OPUS:
tracksElement = createTracksElementWithOpusAudio(TEST_CHANNEL_COUNT);
break;
case ID_VORBIS:
tracksElement = createTracksElementWithVorbisAudio(TEST_CHANNEL_COUNT);
break;
}
byte[] infoElement = createInfoElement(timecodeScale);
byte[] cuesElement = createCuesElement(CUE_POINT_ELEMENT_BYTE_SIZE * cuePoints);
int initalizationSegmentSize = infoElement.length + tracksElement.length
+ cuesElement.length + CUE_POINT_ELEMENT_BYTE_SIZE * cuePoints;
byte[] segmentElement = createSegmentElement(initalizationSegmentSize + mediaSegmentSize);
byte[] bytes = joinByteArrays(createEbmlElement(1, docTypeIsWebm, 2),
segmentElement, infoElement, tracksElement, cuesElement);
for (int i = 0; i < cuePoints; i++) {
bytes = joinByteArrays(bytes, createCuePointElement(10 * i, initalizationSegmentSize));
}
return bytes;
}
private static MediaSegment createMediaSegment(int videoBytesLength, int clusterTimecode,
int blockTimecode, boolean keyframe, boolean invisible, boolean simple,
boolean encrypted, boolean validSignalByte) {
byte[] videoBytes = createVideoBytes(videoBytesLength);
byte[] blockBytes;
if (simple) {
blockBytes = createSimpleBlockElement(videoBytes.length, blockTimecode,
keyframe, invisible, true, encrypted, validSignalByte);
} else {
blockBytes = createBlockElement(videoBytes.length, blockTimecode, invisible, true);
}
byte[] clusterBytes =
createClusterElement(blockBytes.length + videoBytes.length, clusterTimecode);
return new MediaSegment(joinByteArrays(clusterBytes, blockBytes, videoBytes), videoBytes);
}
private static byte[] joinByteArrays(byte[]... byteArrays) {
int length = 0;
for (byte[] byteArray : byteArrays) {
length += byteArray.length;
}
byte[] joined = new byte[length];
length = 0;
for (byte[] byteArray : byteArrays) {
System.arraycopy(byteArray, 0, joined, length, byteArray.length);
length += byteArray.length;
}
return joined;
}
private static byte[] createEbmlElement(
int ebmlReadVersion, boolean docTypeIsWebm, int docTypeReadVersion) {
return createByteArray(
0x1A, 0x45, 0xDF, 0xA3, // EBML
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, // size=15
0x42, 0xF7, // EBMLReadVersion
0x81, ebmlReadVersion, // size=1
0x42, 0x82, // DocType
0x84, 0x77, 0x65, 0x62, docTypeIsWebm ? 0x6D : 0x42, // size=4 value=webm/B
0x42, 0x85, // DocTypeReadVersion
0x81, docTypeReadVersion); // size=1
}
private static byte[] createSegmentElement(int size) {
byte[] sizeBytes = getIntegerBytes(size);
return createByteArray(
0x18, 0x53, 0x80, 0x67, // Segment
0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3]);
}
private static byte[] createInfoElement(int timecodeScale) {
byte[] scaleBytes = getIntegerBytes(timecodeScale);
return createByteArray(
0x15, 0x49, 0xA9, 0x66, // Info
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, // size=19
0x2A, 0xD7, 0xB1, // TimecodeScale
0x84, scaleBytes[0], scaleBytes[1], scaleBytes[2], scaleBytes[3], // size=4
0x44, 0x89, // Duration
0x88, 0x40, 0xC3, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00); // size=8 value=9920.0
}
private static byte[] createTracksElementWithVideo(
boolean codecIsVp9, int pixelWidth, int pixelHeight,
ContentEncodingSettings contentEncodingSettings) {
byte[] widthBytes = getIntegerBytes(pixelWidth);
byte[] heightBytes = getIntegerBytes(pixelHeight);
if (contentEncodingSettings != null) {
byte[] orderBytes = getIntegerBytes(contentEncodingSettings.order);
byte[] scopeBytes = getIntegerBytes(contentEncodingSettings.scope);
byte[] typeBytes = getIntegerBytes(contentEncodingSettings.type);
byte[] algorithmBytes = getIntegerBytes(contentEncodingSettings.algorithm);
byte[] cipherModeBytes = getIntegerBytes(contentEncodingSettings.aesCipherMode);
return createByteArray(
0x16, 0x54, 0xAE, 0x6B, // Tracks
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, // size=72
0xAE, // TrackEntry
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, // size=63
0x86, // CodecID
0x85, 0x56, 0x5F, 0x56, 0x50, codecIsVp9 ? 0x39 : 0x30, // size=5 value=V_VP9/0
0x6D, 0x80, // ContentEncodings
0xA4, // size=36
0x62, 0x40, // ContentEncoding
0xA1, // size=33
0x50, 0x31, // ContentEncodingOrder
0x81, orderBytes[3],
0x50, 0x32, // ContentEncodingScope
0x81, scopeBytes[3],
0x50, 0x33, // ContentEncodingType
0x81, typeBytes[3],
0x50, 0x35, // ContentEncryption
0x92, // size=18
0x47, 0xE1, // ContentEncAlgo
0x81, algorithmBytes[3],
0x47, 0xE2, // ContentEncKeyID
0x84, // size=4
TEST_ENCRYPTION_KEY_ID[0], TEST_ENCRYPTION_KEY_ID[1],
TEST_ENCRYPTION_KEY_ID[2], TEST_ENCRYPTION_KEY_ID[3], // value=binary
0x47, 0xE7, // ContentEncAESSettings
0x84, // size=4
0x47, 0xE8, // AESSettingsCipherMode
0x81, cipherModeBytes[3],
0xE0, // Video
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // size=8
0xB0, // PixelWidth
0x82, widthBytes[2], widthBytes[3], // size=2
0xBA, // PixelHeight
0x82, heightBytes[2], heightBytes[3]); // size=2
} else {
return createByteArray(
0x16, 0x54, 0xAE, 0x6B, // Tracks
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, // size=36
0xAE, // TrackEntry
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1B, // size=27
0x86, // CodecID
0x85, 0x56, 0x5F, 0x56, 0x50, codecIsVp9 ? 0x39 : 0x30, // size=5 value=V_VP9/0
0xE0, // Video
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // size=8
0xB0, // PixelWidth
0x82, widthBytes[2], widthBytes[3], // size=2
0xBA, // PixelHeight
0x82, heightBytes[2], heightBytes[3]); // size=2
}
}
private static byte[] createTracksElementWithOpusAudio(int channelCount) {
byte[] channelCountBytes = getIntegerBytes(channelCount);
return createByteArray(
0x16, 0x54, 0xAE, 0x6B, // Tracks
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, // size=57
0xAE, // TrackEntry
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, // size=48
0x86, // CodecID
0x86, 0x41, 0x5F, 0x4F, 0x50, 0x55, 0x53, // size=6 value=A_OPUS
0x56, 0xAA, // CodecDelay
0x83, 0x63, 0x2E, 0xA0, // size=3 value=6500000
0x56, 0xBB, // SeekPreRoll
0x84, 0x04, 0xC4, 0xB4, 0x00, // size=4 value=80000000
0xE1, // Audio
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, // size=13
0x9F, // Channels
0x81, channelCountBytes[3], // size=1
0xB5, // SamplingFrequency
0x88, 0x40, 0xE7, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, // size=8 value=48000
0x63, 0xA2, // CodecPrivate
0x82, 0x00, 0x00); // size=2
}
private byte[] createTracksElementWithVorbisAudio(int channelCount) {
byte[] channelCountBytes = getIntegerBytes(channelCount);
byte[] tracksElement = createByteArray(
0x16, 0x54, 0xAE, 0x6B, // Tracks
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x9C, // size=4252
0xAE, // TrackEntry
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x93, // size=4243 (36+4207)
0x86, // CodecID
0x88, 0x41, 0x5f, 0x56, 0x4f, 0x52, 0x42, 0x49, 0x53, // size=8 value=A_VORBIS
0xE1, // Audio
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, // size=13
0x9F, // Channels
0x81, channelCountBytes[3], // size=1
0xB5, // SamplingFrequency
0x88, 0x40, 0xE7, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, // size=8 value=48000
0x63, 0xA2, // CodecPrivate
0x50, 0x6F); // size=4207
byte[] codecPrivate = new byte[4207];
try {
getInstrumentation().getContext().getResources().getAssets().open(TEST_VORBIS_CODEC_PRIVATE)
.read(codecPrivate);
} catch (IOException e) {
fail(); // should never happen
}
return joinByteArrays(tracksElement, codecPrivate);
}
private static byte[] createCuesElement(int size) {
byte[] sizeBytes = getIntegerBytes(size);
return createByteArray(
0x1C, 0x53, 0xBB, 0x6B, // Cues
0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3]); // size=31
}
private static byte[] createCuePointElement(int cueTime, int cueClusterPosition) {
byte[] positionBytes = getIntegerBytes(cueClusterPosition);
return createByteArray(
0xBB, // CuePoint
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, // size=22
0xB3, // CueTime
0x81, cueTime, // size=1
0xB7, // CueTrackPositions
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, // size=10
0xF1, // CueClusterPosition
0x88, 0x00, 0x00, 0x00, 0x00, positionBytes[0], positionBytes[1],
positionBytes[2], positionBytes[3]); // size=8
}
private static byte[] createClusterElement(int size, int timecode) {
byte[] sizeBytes = getIntegerBytes(size);
byte[] timeBytes = getIntegerBytes(timecode);
return createByteArray(
0x1F, 0x43, 0xB6, 0x75, // Cluster
0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3],
0xE7, // Timecode
0x84, timeBytes[0], timeBytes[1], timeBytes[2], timeBytes[3]); // size=4
}
private static byte[] createSimpleBlockElement(
int size, int timecode, boolean keyframe, boolean invisible, boolean noLacing,
boolean encrypted, boolean validSignalByte) {
byte[] sizeBytes = getIntegerBytes(size + 4 + (encrypted ? 9 : 0));
byte[] timeBytes = getIntegerBytes(timecode);
byte flags = (byte)
((keyframe ? 0x80 : 0x00) | (invisible ? 0x08 : 0x00) | (noLacing ? 0x00 : 0x06));
byte[] simpleBlock = createByteArray(
0xA3, // SimpleBlock
0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3],
0x81, // Track number value=1
timeBytes[2], timeBytes[3], flags); // Timecode and flags
if (encrypted) {
simpleBlock = joinByteArrays(
simpleBlock, createByteArray(validSignalByte ? 0x01 : 0x80),
Arrays.copyOfRange(TEST_INITIALIZATION_VECTOR, 0, 8));
}
return simpleBlock;
}
private static byte[] createBlockElement(
int size, int timecode, boolean invisible, boolean noLacing) {
int blockSize = size + 4;
byte[] blockSizeBytes = getIntegerBytes(blockSize);
byte[] timeBytes = getIntegerBytes(timecode);
int blockElementSize = 1 + 8 + blockSize; // id + size + length of data
byte[] sizeBytes = getIntegerBytes(blockElementSize);
byte flags = (byte) ((invisible ? 0x08 : 0x00) | (noLacing ? 0x00 : 0x06));
return createByteArray(
0xA0, // BlockGroup
0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3],
0xA1, // Block
0x01, 0x00, 0x00, 0x00,
blockSizeBytes[0], blockSizeBytes[1], blockSizeBytes[2], blockSizeBytes[3],
0x81, // Track number value=1
timeBytes[2], timeBytes[3], flags); // Timecode and flags
}
private static byte[] createVideoBytes(int size) {
byte[] videoBytes = new byte[size];
for (int i = 0; i < size; i++) {
videoBytes[i] = (byte) i;
}
return videoBytes;
}
private static byte[] getIntegerBytes(int value) {
return createByteArray(
(value & 0xFF000000) >> 24,
(value & 0x00FF0000) >> 16,
(value & 0x0000FF00) >> 8,
(value & 0x000000FF));
}
private static byte[] createByteArray(int... intArray) {
byte[] byteArray = new byte[intArray.length];
for (int i = 0; i < byteArray.length; i++) {
byteArray[i] = (byte) intArray[i];
}
return byteArray;
}
/** Used by {@link #createMediaSegment} to return both cluster and video bytes together. */
private static final class MediaSegment {
private final byte[] clusterBytes;
private final byte[] videoBytes;
private MediaSegment(byte[] clusterBytes, byte[] videoBytes) {
this.clusterBytes = clusterBytes;
this.videoBytes = videoBytes;
}
}
/** Used by {@link #assertIndex(IndexPoint...)} to validate index elements. */
private static final class IndexPoint {
private final long timeUs;
private final int size;
private final long durationUs;
private IndexPoint(long timeUs, int size, long durationUs) {
this.timeUs = timeUs;
this.size = size;
this.durationUs = durationUs;
}
}
/** Used by {@link #createTracksElementWithVideo} to create a Track header with Encryption. */
private static final class ContentEncodingSettings {
private final int order;
private final int scope;
private final int type;
private final int algorithm;
private final int aesCipherMode;
private ContentEncodingSettings(int order, int scope, int type, int algorithm,
int aesCipherMode) {
this.order = order;
this.scope = scope;
this.type = type;
this.algorithm = algorithm;
this.aesCipherMode = aesCipherMode;
}
}
/** Implements {@link ExtractorOutput} and {@link TrackOutput} for test purposes. */
public static class TestOutput implements ExtractorOutput, TrackOutput {
public boolean tracksEnded;
public SeekMap seekMap;
public DrmInitData drmInitData;
public MediaFormat format;
private long sampleTimeUs;
private int sampleFlags;
private byte[] sampleData;
@Override
public TrackOutput track(int trackId) {
return this;
}
@Override
public void endTracks() {
tracksEnded = true;
}
@Override
public void seekMap(SeekMap seekMap) {
this.seekMap = seekMap;
}
@Override
public void drmInitData(DrmInitData drmInitData) {
this.drmInitData = drmInitData;
}
@Override
public void format(MediaFormat format) {
this.format = format;
}
@Override
public int sampleData(ExtractorInput input, int length) throws IOException,
InterruptedException {
byte[] newData = new byte[length];
input.readFully(newData, 0, length);
sampleData = sampleData == null ? newData : joinByteArrays(sampleData, newData);
return length;
}
@Override
public void sampleData(ParsableByteArray data, int length) {
byte[] newData = new byte[length];
data.readBytes(newData, 0, length);
sampleData = sampleData == null ? newData : joinByteArrays(sampleData, newData);
}
@Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
this.sampleTimeUs = timeUs;
this.sampleFlags = flags;
}
}
}
/*
* 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.testutil;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.Assertions;
import java.io.IOException;
import java.util.ArrayList;
/**
* A fake {@link DataSource} capable of simulating various scenarios.
* <p>
* The data that will be read from the source can be constructed by calling
* {@link Builder#appendReadData(byte[])}. Calls to {@link #read(byte[], int, int)} will not span
* the boundaries between arrays passed to successive calls, and hence the boundaries control the
* positions at which read requests to the source may only be partially satisfied.
* <p>
* Errors can be inserted by calling {@link Builder#appendReadError(IOException)}. An inserted error
* will be thrown from the first call to {@link #read(byte[], int, int)} that attempts to read from
* the corresponding position, and from all subsequent calls to {@link #read(byte[], int, int)}
* until the source is closed. If the source is closed and re-opened having encountered an error,
* that error will not be thrown again.
*/
public final class FakeDataSource implements DataSource {
private final ArrayList<Segment> segments;
private final boolean simulateUnknownLength;
private final long totalLength;
private boolean opened;
private int currentSegmentIndex;
private long bytesRemaining;
public FakeDataSource(boolean simulateUnknownLength, ArrayList<Segment> segments) {
this.simulateUnknownLength = simulateUnknownLength;
this.segments = segments;
long totalLength = 0;
for (Segment segment : segments) {
totalLength += segment.length;
}
this.totalLength = totalLength;
}
@Override
public long open(DataSpec dataSpec) throws IOException {
Assertions.checkState(!opened);
// DataSpec requires a matching close call even if open fails.
opened = true;
// If the source knows that the request is unsatisfiable then fail.
if (dataSpec.position >= totalLength) {
throw new IOException("Unsatisfiable position");
} else if (dataSpec.length != C.LENGTH_UNBOUNDED
&& dataSpec.position + dataSpec.length >= totalLength) {
throw new IOException("Unsatisfiable range");
}
// Scan through the segments, configuring them for the current read.
boolean findingCurrentSegmentIndex = true;
currentSegmentIndex = 0;
int scannedLength = 0;
for (Segment segment : segments) {
segment.bytesRead =
(int) Math.min(Math.max(0, dataSpec.position - scannedLength), segment.length);
scannedLength += segment.length;
findingCurrentSegmentIndex &= segment.isErrorSegment() ? segment.exceptionCleared
: segment.bytesRead == segment.length;
if (findingCurrentSegmentIndex) {
currentSegmentIndex++;
}
}
// Configure bytesRemaining, and return.
if (dataSpec.length == C.LENGTH_UNBOUNDED) {
bytesRemaining = totalLength - dataSpec.position;
return simulateUnknownLength ? C.LENGTH_UNBOUNDED : bytesRemaining;
} else {
bytesRemaining = dataSpec.length;
return bytesRemaining;
}
}
@Override
public void close() throws IOException {
Assertions.checkState(opened);
opened = false;
if (currentSegmentIndex < segments.size()) {
Segment current = segments.get(currentSegmentIndex);
if (current.isErrorSegment() && current.exceptionThrown) {
current.exceptionCleared = true;
}
}
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
Assertions.checkState(opened);
while (true) {
if (currentSegmentIndex == segments.size() || bytesRemaining == 0) {
return -1;
}
Segment current = segments.get(currentSegmentIndex);
if (current.exception != null) {
if (!current.exceptionCleared) {
current.exceptionThrown = true;
throw current.exception;
} else {
currentSegmentIndex++;
}
} else {
// Read at most bytesRemaining.
readLength = (int) Math.min(readLength, bytesRemaining);
// Do not allow crossing of the segment boundary.
readLength = Math.min(readLength, current.length - current.bytesRead);
// Perform the read and return.
System.arraycopy(current.data, current.bytesRead, buffer, offset, readLength);
bytesRemaining -= readLength;
current.bytesRead += readLength;
if (current.bytesRead == current.length) {
currentSegmentIndex++;
}
return readLength;
}
}
}
private static class Segment {
public final IOException exception;
public final byte[] data;
public final int length;
private boolean exceptionThrown;
private boolean exceptionCleared;
private int bytesRead;
public Segment(byte[] data, IOException exception) {
this.data = data;
this.exception = exception;
length = data != null ? data.length : 0;
}
public boolean isErrorSegment() {
return exception != null;
}
}
/**
* Builder of {@link FakeDataSource} instances.
*/
public static class Builder {
private final ArrayList<Segment> segments;
private boolean simulateUnknownLength;
public Builder() {
segments = new ArrayList<Segment>();
}
/**
* When set, {@link FakeDataSource#open(DataSpec)} will behave as though the source is unable to
* determine the length of the underlying data. Hence the return value will always be equal to
* the {@link DataSpec#length} of the argument, including the case where the length is equal to
* {@link C#LENGTH_UNBOUNDED}.
*/
public Builder setSimulateUnknownLength(boolean simulateUnknownLength) {
this.simulateUnknownLength = simulateUnknownLength;
return this;
}
/**
* Appends to the underlying data.
*/
public Builder appendReadData(byte[] data) {
Assertions.checkState(data != null && data.length > 0);
segments.add(new Segment(data, null));
return this;
}
/**
* Appends an error in the underlying data.
*/
public Builder appendReadError(IOException exception) {
segments.add(new Segment(null, exception));
return this;
}
public FakeDataSource build() {
return new FakeDataSource(simulateUnknownLength, segments);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment