Commit 4a1fed9e by Oliver Woodman

Add new style WebM extractor.

parent 6c5af232
Showing with 1149 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 {
}
@Override
public int sampleData(ExtractorInput input, int length) throws IOException, InterruptedException {
return rollingBuffer.appendData(input, length);
}
@Override
public void sampleData(ParsableByteArray buffer, int length) {
rollingBuffer.appendData(buffer, length);
}
......
......@@ -55,4 +55,14 @@ public interface Extractor {
*/
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 @@
*/
package com.google.android.exoplayer.extractor;
import com.google.android.exoplayer.drm.DrmInitData;
/**
* Receives stream level data extracted by an {@link Extractor}.
*/
......@@ -36,4 +38,18 @@ public interface ExtractorOutput {
*/
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;
/**
* 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 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;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/**
* Receives track level data extracted by an {@link Extractor}.
*/
......@@ -34,6 +36,17 @@ public interface TrackOutput {
/**
* 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 length The number of bytes to read.
*/
......@@ -43,14 +56,14 @@ public interface TrackOutput {
* Invoked when metadata associated with a sample has been extracted from the stream.
* <p>
* 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 flags Flags associated with the sample. See {@link SampleHolder#flags}.
* @param size The size of the sample data, in bytes.
* @param offset The number of bytes that have been passed to
* {@link #sampleData(ParsableByteArray, int)} since the last byte belonging to the sample
* whose metadata is being passed.
* {@link #sampleData(ExtractorInput, int)} or {@link #sampleData(ParsableByteArray, int)}
* 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.
*/
void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey);
......
......@@ -50,8 +50,12 @@ public class AdtsExtractor implements Extractor {
}
@Override
public int read(ExtractorInput input)
throws IOException, InterruptedException {
public void seek() {
throw new UnsupportedOperationException();
}
@Override
public int read(ExtractorInput input) throws IOException, InterruptedException {
int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
if (bytesRead == -1) {
return RESULT_END_OF_INPUT;
......
......@@ -72,8 +72,12 @@ public final class TsExtractor implements Extractor {
}
@Override
public int read(ExtractorInput input)
throws IOException, InterruptedException {
public void seek() {
throw new UnsupportedOperationException();
}
@Override
public int read(ExtractorInput input) throws IOException, InterruptedException {
if (!input.readFully(tsPacketBuffer.data, 0, TS_PACKET_SIZE, true)) {
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;
}
}
......@@ -17,10 +17,12 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.MediaFormat;
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.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.upstream.BufferPool;
import com.google.android.exoplayer.util.Assertions;
......@@ -209,4 +211,14 @@ public final class HlsExtractorWrapper implements ExtractorOutput {
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.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