diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/BufferingInput.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/BufferingInput.java
new file mode 100644
index 0000000000..f628b70de3
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/BufferingInput.java
@@ -0,0 +1,203 @@
+/*
+ * 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.mp3;
+
+import com.google.android.exoplayer.extractor.ExtractorInput;
+import com.google.android.exoplayer.extractor.TrackOutput;
+import com.google.android.exoplayer.util.ParsableByteArray;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.BufferOverflowException;
+
+/**
+ * Buffers bytes read from an {@link ExtractorInput} to allow re-reading buffered bytes within a
+ * window starting at a marked position.
+ */
+/* package */ final class BufferingInput {
+
+ private final ParsableByteArray buffer;
+ private final int capacity;
+
+ private int readPosition;
+ private int writePosition;
+ private int markPosition;
+
+ /**
+ * Constructs a new buffer for reading from extractor inputs that can store up to {@code capacity}
+ * bytes.
+ *
+ * @param capacity Number of bytes that can be stored in the buffer.
+ */
+ public BufferingInput(int capacity) {
+ this.capacity = capacity;
+ buffer = new ParsableByteArray(capacity * 2);
+ }
+
+ /** Discards any pending data in the buffer and returns the writing position to zero. */
+ public void reset() {
+ readPosition = 0;
+ writePosition = 0;
+ markPosition = 0;
+ }
+
+ /**
+ * Moves the mark to be at the reading position. Any data before the reading position is
+ * discarded. After calling this method, calling {@link #returnToMark} will move the reading
+ * position back to the mark position.
+ */
+ public void mark() {
+ if (readPosition > capacity) {
+ System.arraycopy(buffer.data, readPosition, buffer.data, 0, writePosition - readPosition);
+ writePosition -= readPosition;
+ readPosition = 0;
+ }
+ markPosition = readPosition;
+ }
+
+ /** Moves the reading position back to the mark position. */
+ public void returnToMark() {
+ readPosition = markPosition;
+ }
+
+ /** Returns the number of bytes available for reading from the current position. */
+ public int getAvailableByteCount() {
+ return writePosition - readPosition;
+ }
+
+ /**
+ * Buffers any more data required to read {@code length} bytes from the reading position, and
+ * returns a {@link ParsableByteArray} that wraps the buffer's byte array, with its position set
+ * to the current reading position. The read position is then updated for having read
+ * {@code length} bytes.
+ *
+ * @param extractorInput {@link ExtractorInput} from which additional data should be read.
+ * @param length Number of bytes that will be readable in the returned array.
+ * @return {@link ParsableByteArray} from which {@code length} bytes can be read.
+ * @throws IOException Thrown if there was an error reading from the stream.
+ * @throws InterruptedException Thrown if reading from the stream was interrupted.
+ */
+ public ParsableByteArray getParsableByteArray(ExtractorInput extractorInput, int length)
+ throws IOException, InterruptedException {
+ if (!ensureLoaded(extractorInput, length)) {
+ throw new EOFException();
+ }
+ ParsableByteArray parsableByteArray = new ParsableByteArray(buffer.data, writePosition);
+ parsableByteArray.setPosition(readPosition);
+ readPosition += length;
+ return parsableByteArray;
+ }
+
+ /**
+ * Drains as much buffered data as possible up to {@code length} bytes to {@code trackOutput}.
+ *
+ * @param trackOutput Track output to populate with up to {@code length} bytes of sample data.
+ * @param length Number of bytes to try to read from the buffer.
+ * @return The number of buffered bytes written.
+ */
+ public int drainToOutput(TrackOutput trackOutput, int length) {
+ if (length == 0) {
+ return 0;
+ }
+ buffer.setPosition(readPosition);
+ int bytesToDrain = Math.min(writePosition - readPosition, length);
+ trackOutput.sampleData(buffer, bytesToDrain);
+ readPosition += bytesToDrain;
+ return bytesToDrain;
+ }
+
+ /**
+ * Skips {@code length} bytes from the reading position, reading from {@code extractorInput} to
+ * populate the buffer if required.
+ *
+ * @param extractorInput {@link ExtractorInput} from which additional data should be read.
+ * @param length Number of bytes to skip.
+ * @throws IOException Thrown if there was an error reading from the stream.
+ * @throws InterruptedException Thrown if reading from the stream was interrupted.
+ */
+ public void skip(ExtractorInput extractorInput, int length)
+ throws IOException, InterruptedException {
+ if (!readInternal(extractorInput, null, 0, length)) {
+ throw new EOFException();
+ }
+ }
+
+ /**
+ * Reads {@code length} bytes from the reading position, reading from {@code extractorInput} to
+ * populate the buffer if required.
+ *
+ * @param extractorInput {@link ExtractorInput} from which additional data should be read.
+ * @param length Number of bytes to read.
+ * @throws IOException Thrown if there was an error reading from the stream.
+ * @throws InterruptedException Thrown if reading from the stream was interrupted.
+ * @throws EOFException Thrown if the end of the file was reached.
+ */
+ public void read(ExtractorInput extractorInput, byte[] target, int offset, int length)
+ throws IOException, InterruptedException {
+ if (!readInternal(extractorInput, target, offset, length)) {
+ throw new EOFException();
+ }
+ }
+
+ /**
+ * Reads {@code length} bytes from the reading position, reading from {@code extractorInput} to
+ * populate the buffer if required.
+ *
+ *
Returns {@code false} if the end of the stream has been reached. Throws {@link EOFException}
+ * if the read request could only be partially satisfied. Returns {@code true} otherwise.
+ *
+ * @param extractorInput {@link ExtractorInput} from which additional data should be read.
+ * @param length Number of bytes to read.
+ * @return Whether the extractor input is at the end of the stream.
+ * @throws IOException Thrown if there was an error reading from the stream.
+ * @throws InterruptedException Thrown if reading from the stream was interrupted.
+ * @throws EOFException Thrown if the end of the file was reached.
+ */
+ public boolean readAllowingEndOfInput(ExtractorInput extractorInput, byte[] target, int offset,
+ int length) throws IOException, InterruptedException {
+ return readInternal(extractorInput, target, offset, length);
+ }
+
+ private boolean readInternal(ExtractorInput extractorInput, byte[] target, int offset, int length)
+ throws InterruptedException, IOException {
+ if (!ensureLoaded(extractorInput, length)) {
+ return false;
+ }
+ if (target != null) {
+ System.arraycopy(buffer.data, readPosition, target, offset, length);
+ }
+ readPosition += length;
+ return true;
+ }
+
+ /** Ensures the buffer contains enough data to read {@code length} bytes. */
+ private boolean ensureLoaded(ExtractorInput extractorInput, int length)
+ throws InterruptedException, IOException {
+ if (length + readPosition - markPosition > capacity) {
+ throw new BufferOverflowException();
+ }
+
+ int bytesToLoad = length - (writePosition - readPosition);
+ if (bytesToLoad > 0) {
+ if (!extractorInput.readFully(buffer.data, writePosition, bytesToLoad, true)) {
+ return false;
+ }
+ writePosition += bytesToLoad;
+ }
+ return true;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/ConstantBitrateSeeker.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/ConstantBitrateSeeker.java
new file mode 100644
index 0000000000..cc705819e2
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/ConstantBitrateSeeker.java
@@ -0,0 +1,55 @@
+/*
+ * 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.mp3;
+
+import com.google.android.exoplayer.C;
+
+/**
+ * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate.
+ */
+/* package */ final class ConstantBitrateSeeker implements Mp3Extractor.Seeker {
+
+ private static final int MICROSECONDS_PER_SECOND = 1000000;
+ private static final int BITS_PER_BYTE = 8;
+
+ private final long firstFramePosition;
+ private final int bitrate;
+ private final long durationUs;
+
+ public ConstantBitrateSeeker(long firstFramePosition, int bitrate, long inputLength) {
+ this.firstFramePosition = firstFramePosition;
+ this.bitrate = bitrate;
+
+ durationUs =
+ inputLength == C.LENGTH_UNBOUNDED ? C.UNKNOWN_TIME_US : getTimeUs(inputLength);
+ }
+
+ @Override
+ public long getPosition(long timeUs) {
+ return firstFramePosition + (timeUs * bitrate) / (MICROSECONDS_PER_SECOND * BITS_PER_BYTE);
+ }
+
+ @Override
+ public long getTimeUs(long position) {
+ return ((position - firstFramePosition) * MICROSECONDS_PER_SECOND * BITS_PER_BYTE) / bitrate;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java
new file mode 100644
index 0000000000..177cbea306
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java
@@ -0,0 +1,289 @@
+/*
+ * 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.mp3;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.ParserException;
+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.util.MimeTypes;
+import com.google.android.exoplayer.util.ParsableByteArray;
+import com.google.android.exoplayer.util.Util;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Collections;
+
+/**
+ * Extracts data from an MP3 file.
+ */
+public final class Mp3Extractor implements Extractor {
+
+ /**
+ * {@link SeekMap} that also allows mapping from position (byte offset) back to time, which can be
+ * used to work out the new sample basis timestamp after seeking and resynchronization.
+ */
+ /* package */ interface Seeker extends SeekMap {
+
+ /**
+ * Maps a position (byte offset) to a corresponding sample timestamp.
+ *
+ * @param position A seek position (byte offset) relative to the start of the stream.
+ * @return The corresponding timestamp of the next sample to be read, in microseconds.
+ */
+ long getTimeUs(long position);
+
+ /** Returns the duration of the source, in microseconds. */
+ long getDurationUs();
+
+ }
+
+ /** The maximum number of bytes to search when synchronizing, before giving up. */
+ private static final int MAX_BYTES_TO_SEARCH = 128 * 1024;
+
+ /** Mask that includes the audio header values that must match between frames. */
+ private static final int HEADER_MASK = 0xFFFE0C00;
+ private static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
+ private static final String[] MIME_TYPE_BY_LAYER =
+ new String[] {MimeTypes.AUDIO_MPEG_L1, MimeTypes.AUDIO_MPEG_L2, MimeTypes.AUDIO_MPEG};
+
+ /**
+ * Theoretical maximum frame size for an MPEG audio stream, which occurs when playing a Layer 2
+ * MPEG 2.5 audio stream at 16 kb/s (with padding). The size is 1152 sample/frame *
+ * 160000 bit/s / (8000 sample/s * 8 bit/byte) + 1 padding byte/frame = 2881 byte/frame.
+ * The next power of two size is 4 KiB.
+ */
+ private static final int MAX_FRAME_SIZE_BYTES = 4096;
+
+ private final BufferingInput inputBuffer;
+ private final ParsableByteArray scratch;
+ private final MpegAudioHeader synchronizedHeader;
+
+ // Extractor outputs.
+ private ExtractorOutput extractorOutput;
+ private TrackOutput trackOutput;
+
+ private int synchronizedHeaderData;
+
+ private Seeker seeker;
+ private long basisTimeUs;
+ private int samplesRead;
+ private int sampleBytesRemaining;
+
+ /** Constructs a new {@link Mp3Extractor}. */
+ public Mp3Extractor() {
+ inputBuffer = new BufferingInput(MAX_FRAME_SIZE_BYTES * 3);
+ scratch = new ParsableByteArray(4);
+ synchronizedHeader = new MpegAudioHeader();
+ }
+
+ @Override
+ public void init(ExtractorOutput extractorOutput) {
+ this.extractorOutput = extractorOutput;
+ trackOutput = extractorOutput.track(0);
+ extractorOutput.endTracks();
+ }
+
+ @Override
+ public void seek() {
+ synchronizedHeaderData = 0;
+ samplesRead = 0;
+ basisTimeUs = -1;
+ sampleBytesRemaining = 0;
+ inputBuffer.reset();
+ }
+
+ @Override
+ public int read(ExtractorInput extractorInput) throws IOException, InterruptedException {
+ if (synchronizedHeaderData == 0
+ && synchronizeCatchingEndOfInput(extractorInput) == RESULT_END_OF_INPUT) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ return readSample(extractorInput);
+ }
+
+ private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
+ if (sampleBytesRemaining == 0) {
+ long headerPosition = maybeResynchronize(extractorInput);
+ if (headerPosition == RESULT_END_OF_INPUT) {
+ return RESULT_END_OF_INPUT;
+ }
+ if (basisTimeUs == -1) {
+ basisTimeUs = seeker.getTimeUs(getPosition(extractorInput, inputBuffer));
+ }
+ sampleBytesRemaining = synchronizedHeader.frameSize;
+ }
+
+ long timeUs = basisTimeUs + (samplesRead * 1000000L / synchronizedHeader.sampleRate);
+
+ // Start by draining any buffered bytes, then read directly from the extractor input.
+ sampleBytesRemaining -= inputBuffer.drainToOutput(trackOutput, sampleBytesRemaining);
+ if (sampleBytesRemaining > 0) {
+ inputBuffer.mark();
+
+ // Return if we still need more data.
+ sampleBytesRemaining -= trackOutput.sampleData(extractorInput, sampleBytesRemaining);
+ if (sampleBytesRemaining > 0) {
+ return RESULT_CONTINUE;
+ }
+ }
+
+ trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, synchronizedHeader.frameSize, 0, null);
+ samplesRead += synchronizedHeader.samplesPerFrame;
+ sampleBytesRemaining = 0;
+ return RESULT_CONTINUE;
+ }
+
+ /** Attempts to read an MPEG audio header at the current offset, resynchronizing if necessary. */
+ private long maybeResynchronize(ExtractorInput extractorInput)
+ throws IOException, InterruptedException {
+ inputBuffer.mark();
+ if (!inputBuffer.readAllowingEndOfInput(extractorInput, scratch.data, 0, 4)) {
+ return RESULT_END_OF_INPUT;
+ }
+ inputBuffer.returnToMark();
+
+ scratch.setPosition(0);
+ int sampleHeaderData = scratch.readInt();
+ if ((sampleHeaderData & HEADER_MASK) == (synchronizedHeaderData & HEADER_MASK)) {
+ int frameSize = MpegAudioHeader.getFrameSize(sampleHeaderData);
+ if (frameSize != -1) {
+ MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader);
+ return RESULT_CONTINUE;
+ }
+ }
+
+ synchronizedHeaderData = 0;
+ inputBuffer.skip(extractorInput, 1);
+ return synchronizeCatchingEndOfInput(extractorInput);
+ }
+
+ private long synchronizeCatchingEndOfInput(ExtractorInput extractorInput)
+ throws IOException, InterruptedException {
+ // An EOFException will be raised if any read operation was partially satisfied. If a seek
+ // operation resulted in reading from within the last frame, we may try to read past the end of
+ // the file in a partially-satisfied read operation, so we need to catch the exception.
+ try {
+ return synchronize(extractorInput);
+ } catch (EOFException e) {
+ return RESULT_END_OF_INPUT;
+ }
+ }
+
+ private long synchronize(ExtractorInput extractorInput) throws IOException, InterruptedException {
+ long startPosition = getPosition(extractorInput, inputBuffer);
+
+ // Skip any ID3 header at the start of the file.
+ if (startPosition == 0) {
+ inputBuffer.read(extractorInput, scratch.data, 0, 3);
+ scratch.setPosition(0);
+ if (scratch.readUnsignedInt24() == ID3_TAG) {
+ extractorInput.skipFully(3);
+ extractorInput.readFully(scratch.data, 0, 4);
+ int headerLength = ((scratch.data[0] & 0x7F) << 21) | ((scratch.data[1] & 0x7F) << 14)
+ | ((scratch.data[2] & 0x7F) << 7) | (scratch.data[3] & 0x7F);
+ extractorInput.skipFully(headerLength);
+ inputBuffer.reset();
+ startPosition = getPosition(extractorInput, inputBuffer);
+ } else {
+ inputBuffer.returnToMark();
+ }
+ }
+
+ // Try to find four consecutive valid MPEG audio frames.
+ inputBuffer.mark();
+ long headerPosition = startPosition;
+ int validFrameCount = 0;
+ while (true) {
+ if (headerPosition - startPosition >= MAX_BYTES_TO_SEARCH) {
+ throw new ParserException("Searched too many bytes while resynchronizing.");
+ }
+
+ if (!inputBuffer.readAllowingEndOfInput(extractorInput, scratch.data, 0, 4)) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ scratch.setPosition(0);
+ int headerData = scratch.readInt();
+ int frameSize;
+ if ((synchronizedHeaderData != 0
+ && (headerData & HEADER_MASK) != (synchronizedHeaderData & HEADER_MASK))
+ || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == -1) {
+ validFrameCount = 0;
+ synchronizedHeaderData = 0;
+
+ // Try reading a header starting at the next byte.
+ inputBuffer.returnToMark();
+ inputBuffer.skip(extractorInput, 1);
+ inputBuffer.mark();
+ headerPosition++;
+ continue;
+ }
+
+ if (validFrameCount == 0) {
+ MpegAudioHeader.populateHeader(headerData, synchronizedHeader);
+ synchronizedHeaderData = headerData;
+ }
+
+ // The header was valid and matching (if appropriate). Check another or end synchronization.
+ validFrameCount++;
+ if (validFrameCount == 4) {
+ break;
+ }
+
+ // Look for more headers.
+ inputBuffer.skip(extractorInput, frameSize - 4);
+ }
+
+ // The input buffer read position is now synchronized.
+ inputBuffer.returnToMark();
+ if (seeker == null) {
+ ParsableByteArray frame =
+ inputBuffer.getParsableByteArray(extractorInput, synchronizedHeader.frameSize);
+ seeker = XingSeeker.create(synchronizedHeader, frame, headerPosition,
+ extractorInput.getLength());
+ if (seeker == null) {
+ seeker = VbriSeeker.create(synchronizedHeader, frame, headerPosition);
+ }
+ if (seeker == null) {
+ inputBuffer.returnToMark();
+ seeker = new ConstantBitrateSeeker(
+ headerPosition, synchronizedHeader.bitrate * 1000, extractorInput.getLength());
+ } else {
+ // Discard the frame that was parsed for seeking metadata.
+ inputBuffer.mark();
+ }
+ extractorOutput.seekMap(seeker);
+ trackOutput.format(MediaFormat.createAudioFormat(
+ MIME_TYPE_BY_LAYER[synchronizedHeader.layerIndex], MAX_FRAME_SIZE_BYTES,
+ seeker.getDurationUs(), synchronizedHeader.channels, synchronizedHeader.sampleRate,
+ synchronizedHeader.bitrate * 1000, Collections.emptyList()));
+ }
+
+ return headerPosition;
+ }
+
+ /** Returns the reading position of {@code bufferingInput} relative to the extractor's stream. */
+ private static long getPosition(ExtractorInput extractorInput, BufferingInput bufferingInput) {
+ return extractorInput.getPosition() - bufferingInput.getAvailableByteCount();
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/MpegAudioHeader.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/MpegAudioHeader.java
new file mode 100644
index 0000000000..9f05d0e3a6
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/MpegAudioHeader.java
@@ -0,0 +1,189 @@
+/*
+ * 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.mp3;
+
+/** Parsed MPEG audio frame header. */
+/* package */ final class MpegAudioHeader {
+
+ private static final int[] SAMPLING_RATE_V1 = {44100, 48000, 32000};
+ private static final int[] BITRATE_V1_L1 =
+ {32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448};
+ private static final int[] BITRATE_V2_L1 =
+ {32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256};
+ private static final int[] BITRATE_V1_L2 =
+ {32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384};
+ private static final int[] BITRATE_V1_L3 =
+ {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320};
+ private static final int[] BITRATE_V2 =
+ {8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160};
+
+ /** Returns the size of the frame associated with {@code header}, or -1 if it is invalid. */
+ public static int getFrameSize(int header) {
+ if ((header & 0xFFE00000) != 0xFFE00000) {
+ return -1;
+ }
+
+ int version = (header >>> 19) & 3;
+ if (version == 1) {
+ return -1;
+ }
+
+ int layer = (header >>> 17) & 3;
+ if (layer == 0) {
+ return -1;
+ }
+
+ int bitrateIndex = (header >>> 12) & 15;
+ if (bitrateIndex == 0 || bitrateIndex == 0xF) {
+ // Disallow "free" bitrate.
+ return -1;
+ }
+
+ int samplingRateIndex = (header >>> 10) & 3;
+ if (samplingRateIndex == 3) {
+ return -1;
+ }
+
+ int samplingRate = SAMPLING_RATE_V1[samplingRateIndex];
+ if (version == 2) {
+ // Version 2
+ samplingRate /= 2;
+ } else if (version == 0) {
+ // Version 2.5
+ samplingRate /= 4;
+ }
+
+ int bitrate;
+ int padding = (header >>> 9) & 1;
+ if (layer == 3) {
+ // Layer I (layer == 3)
+ bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
+ return (12000 * bitrate / samplingRate + padding) * 4;
+ } else {
+ // Layer II (layer == 2) or III (layer == 1)
+ if (version == 3) {
+ bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
+ } else {
+ // Version 2 or 2.5.
+ bitrate = BITRATE_V2[bitrateIndex - 1];
+ }
+ }
+
+ if (version == 3) {
+ // Version 1
+ return 144000 * bitrate / samplingRate + padding;
+ } else {
+ // Version 2 or 2.5
+ return (layer == 1 ? 72000 : 144000) * bitrate / samplingRate + padding;
+ }
+ }
+
+ /**
+ * Returns the header represented by {@code header}, if it is valid; {@code null} otherwise.
+ *
+ * @param headerData Header data to parse.
+ * @param header Header to populate with data from {@code headerData}.
+ */
+ public static void populateHeader(int headerData, MpegAudioHeader header) {
+ if ((headerData & 0xFFE00000) != 0xFFE00000) {
+ return;
+ }
+
+ int version = (headerData >>> 19) & 3;
+ if (version == 1) {
+ return;
+ }
+
+ int layer = (headerData >>> 17) & 3;
+ if (layer == 0) {
+ return;
+ }
+
+ int bitrateIndex = (headerData >>> 12) & 15;
+ if (bitrateIndex == 0 || bitrateIndex == 0xF) {
+ // Disallow "free" bitrate.
+ return;
+ }
+
+ int samplingRateIndex = (headerData >>> 10) & 3;
+ if (samplingRateIndex == 3) {
+ return;
+ }
+
+ int sampleRate = SAMPLING_RATE_V1[samplingRateIndex];
+ if (version == 2) {
+ // Version 2
+ sampleRate /= 2;
+ } else if (version == 0) {
+ // Version 2.5
+ sampleRate /= 4;
+ }
+
+ int padding = (headerData >>> 9) & 1;
+ int bitrate, frameSize, samplesPerFrame;
+ if (layer == 3) {
+ // Layer I (layer == 3)
+ bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
+ frameSize = (12000 * bitrate / sampleRate + padding) * 4;
+ samplesPerFrame = 384;
+ } else {
+ // Layer II (layer == 2) or III (layer == 1)
+ if (version == 3) {
+ // Version 1
+ bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
+ samplesPerFrame = 1152;
+ frameSize = 144000 * bitrate / sampleRate + padding;
+ } else {
+ // Version 2 or 2.5.
+ bitrate = BITRATE_V2[bitrateIndex - 1];
+ samplesPerFrame = layer == 1 ? 576 : 1152;
+ frameSize = (layer == 1 ? 72000 : 144000) * bitrate / sampleRate + padding;
+ }
+ }
+
+ int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;
+ int layerIndex = 3 - layer;
+ header.setValues(
+ version, layerIndex, frameSize, sampleRate, channels, bitrate, samplesPerFrame);
+ }
+
+ /** MPEG audio header version. */
+ public int version;
+ /** MPEG audio layer index, starting at zero. */
+ public int layerIndex;
+ /** Size of the frame associated with this header, in bytes. */
+ public int frameSize;
+ /** Sample rate in samples per second. */
+ public int sampleRate;
+ /** Number of audio channels in the frame. */
+ public int channels;
+ /** Bitrate of the frame in kbit/s. */
+ public int bitrate;
+ /** Number of samples stored in the frame. */
+ public int samplesPerFrame;
+
+ private void setValues(int version, int layerIndex, int frameSize, int sampleRate, int channels,
+ int bitrate, int samplesPerFrame) {
+ this.version = version;
+ this.layerIndex = layerIndex;
+ this.frameSize = frameSize;
+ this.sampleRate = sampleRate;
+ this.channels = channels;
+ this.bitrate = bitrate;
+ this.samplesPerFrame = samplesPerFrame;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/VbriSeeker.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/VbriSeeker.java
new file mode 100644
index 0000000000..cf4d89cf8e
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/VbriSeeker.java
@@ -0,0 +1,118 @@
+/*
+ * 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.mp3;
+
+import com.google.android.exoplayer.util.ParsableByteArray;
+import com.google.android.exoplayer.util.Util;
+
+/**
+ * MP3 seeker that uses metadata from a VBRI header.
+ */
+/* package */ final class VbriSeeker implements Mp3Extractor.Seeker {
+
+ private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI");
+
+ /**
+ * If {@code frame} contains a VBRI header and it is usable for seeking, returns a
+ * {@link Mp3Extractor.Seeker} for seeking in the containing stream. Otherwise, returns
+ * {@code null}, which indicates that the information in the frame was not a VBRI header, or was
+ * unusable for seeking.
+ */
+ public static VbriSeeker create(
+ MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, long position) {
+ long basePosition = position + mpegAudioHeader.frameSize;
+
+ // Read the VBRI header.
+ frame.skip(32);
+ int headerData = frame.readInt();
+ if (headerData != VBRI_HEADER) {
+ return null;
+ }
+ frame.skip(10);
+ int numFrames = frame.readInt();
+ if (numFrames <= 0) {
+ return null;
+ }
+ int sampleRate = mpegAudioHeader.sampleRate;
+ long durationUs = Util.scaleLargeTimestamp(
+ numFrames, 1000000L * (sampleRate >= 32000 ? 1152 : 576), sampleRate);
+ int numEntries = frame.readUnsignedShort();
+ int scale = frame.readUnsignedShort();
+ int entrySize = frame.readUnsignedShort();
+
+ // Read entries in the VBRI header.
+ long[] timesUs = new long[numEntries];
+ long[] offsets = new long[numEntries];
+ long segmentDurationUs = durationUs / numEntries;
+ long now = 0;
+ int segmentIndex = 0;
+ while (segmentIndex < numEntries) {
+ int numBytes;
+ switch (entrySize) {
+ case 1:
+ numBytes = frame.readUnsignedByte();
+ break;
+ case 2:
+ numBytes = frame.readUnsignedShort();
+ break;
+ case 3:
+ numBytes = frame.readUnsignedInt24();
+ break;
+ case 4:
+ numBytes = frame.readUnsignedIntToInt();
+ break;
+ default:
+ return null;
+ }
+ now += segmentDurationUs;
+ timesUs[segmentIndex] = now;
+ position += numBytes * scale;
+ offsets[segmentIndex] = position;
+
+ segmentIndex++;
+ }
+ return new VbriSeeker(timesUs, offsets, basePosition, durationUs);
+ }
+
+ private final long[] timesUs;
+ private final long[] positions;
+ private final long basePosition;
+ private final long durationUs;
+
+ private VbriSeeker(long[] timesUs, long[] positions, long basePosition, long durationUs) {
+ this.timesUs = timesUs;
+ this.positions = positions;
+ this.basePosition = basePosition;
+ this.durationUs = durationUs;
+ }
+
+ @Override
+ public long getPosition(long timeUs) {
+ int index = Util.binarySearchFloor(timesUs, timeUs, false, false);
+ return basePosition + (index == -1 ? 0L : positions[index]);
+ }
+
+ @Override
+ public long getTimeUs(long position) {
+ return timesUs[Util.binarySearchFloor(positions, position, true, true)];
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java
new file mode 100644
index 0000000000..a499ffb0a0
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java
@@ -0,0 +1,167 @@
+/*
+ * 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.mp3;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.util.ParsableByteArray;
+import com.google.android.exoplayer.util.Util;
+
+/**
+ * MP3 seeker that uses metadata from a XING header.
+ */
+/* package */ final class XingSeeker implements Mp3Extractor.Seeker {
+
+ private static final int XING_HEADER = Util.getIntegerCodeForString("Xing");
+ private static final int INFO_HEADER = Util.getIntegerCodeForString("Info");
+
+ /**
+ * If {@code frame} contains a XING header and it is usable for seeking, returns a
+ * {@link Mp3Extractor.Seeker} for seeking in the containing stream. Otherwise, returns
+ * {@code null}, which indicates that the information in the frame was not a XING header, or was
+ * unusable for seeking.
+ */
+ public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame,
+ long position, long inputLength) {
+ int samplesPerFrame = mpegAudioHeader.samplesPerFrame;
+ int sampleRate = mpegAudioHeader.sampleRate;
+ long firstFramePosition = position + mpegAudioHeader.frameSize;
+
+ // Skip to the XING header.
+ int xingBase;
+ if ((mpegAudioHeader.version & 1) == 1) {
+ // MPEG 1.
+ if (mpegAudioHeader.channels != 1) {
+ xingBase = 32;
+ } else {
+ xingBase = 17;
+ }
+ } else {
+ // MPEG 2 or 2.5.
+ if (mpegAudioHeader.channels != 1) {
+ xingBase = 17;
+ } else {
+ xingBase = 9;
+ }
+ }
+ frame.skip(4 + xingBase);
+ int headerData = frame.readInt();
+ if (headerData != XING_HEADER && headerData != INFO_HEADER) {
+ return null;
+ }
+
+ int flags = frame.readInt();
+ // Frame count, size and table of contents are required to use this header.
+ if ((flags & 0x07) != 0x07) {
+ return null;
+ }
+
+ // Read frame count, as (flags & 1) == 1.
+ int frameCount = frame.readUnsignedIntToInt();
+ if (frameCount == 0) {
+ return null;
+ }
+ long durationUs =
+ Util.scaleLargeTimestamp(frameCount, samplesPerFrame * 1000000L, sampleRate);
+
+ // Read size in bytes, as (flags & 2) == 2.
+ long sizeBytes = frame.readUnsignedIntToInt();
+
+ // Read table-of-contents as (flags & 4) == 4.
+ frame.skip(1);
+ long[] tableOfContents = new long[99];
+ for (int i = 0; i < 99; i++) {
+ tableOfContents[i] = frame.readUnsignedByte();
+ }
+
+ // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes:
+ // delay = ((frame.readUnsignedByte() & 0xFF) << 4) + ((frame.readUnsignedByte() & 0xFF) >>> 4);
+ // padding = ((frame.readUnsignedByte() & 0x0F) << 8) + (frame.readUnsignedByte() & 0xFF);
+ return new XingSeeker(tableOfContents, firstFramePosition, sizeBytes, durationUs, inputLength);
+ }
+
+ /** Entries are in the range [0, 255], but are stored as long integers for convenience. */
+ private final long[] tableOfContents;
+ private final long firstFramePosition;
+ private final long sizeBytes;
+ private final long durationUs;
+ private final long inputLength;
+
+ private XingSeeker(long[] tableOfContents, long firstFramePosition, long sizeBytes,
+ long durationUs, long inputLength) {
+ this.tableOfContents = tableOfContents;
+ this.firstFramePosition = firstFramePosition;
+ this.sizeBytes = sizeBytes;
+ this.durationUs = durationUs;
+ this.inputLength = inputLength;
+ }
+
+ @Override
+ public long getPosition(long timeUs) {
+ float percent = timeUs * 100f / durationUs;
+ float fx;
+ if (percent <= 0f) {
+ fx = 0f;
+ } else if (percent >= 100f) {
+ fx = 256f;
+ } else {
+ int a = (int) percent;
+ float fa, fb;
+ if (a == 0) {
+ fa = 0f;
+ } else {
+ fa = tableOfContents[a - 1];
+ }
+ if (a < 99) {
+ fb = tableOfContents[a];
+ } else {
+ fb = 256f;
+ }
+ fx = fa + (fb - fa) * (percent - a);
+ }
+
+ long position = (long) ((1f / 256) * fx * sizeBytes) + firstFramePosition;
+ return inputLength != C.LENGTH_UNBOUNDED ? Math.min(position, inputLength) : position;
+ }
+
+ @Override
+ public long getTimeUs(long position) {
+ long offsetByte = 256 * (position - firstFramePosition) / sizeBytes;
+ int previousIndex = Util.binarySearchFloor(tableOfContents, offsetByte, true, false);
+ long previousTime = getTimeUsForTocIndex(previousIndex);
+ if (previousIndex == 98) {
+ return previousTime;
+ }
+
+ // Linearly interpolate the time taking into account the next entry.
+ long previousByte = previousIndex == -1 ? 0 : tableOfContents[previousIndex];
+ long nextByte = tableOfContents[previousIndex + 1];
+ long nextTime = getTimeUsForTocIndex(previousIndex + 1);
+ long timeOffset =
+ (nextTime - previousTime) * (offsetByte - previousByte) / (nextByte - previousByte);
+ return previousTime + timeOffset;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ /** Returns the time in microseconds corresponding to an index in the table of contents. */
+ private long getTimeUsForTocIndex(int tocIndex) {
+ return durationUs * (tocIndex + 1) / 100;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java
index 6160427890..9c8db64a90 100644
--- a/library/src/main/java/com/google/android/exoplayer/util/Util.java
+++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java
@@ -460,4 +460,19 @@ public final class Util {
}
}
+ /**
+ * Returns the integer equal to the big-endian concatenation of the characters in {@code string}
+ * as bytes. {@code string} must contain four or fewer characters.
+ */
+ public static int getIntegerCodeForString(String string) {
+ int length = string.length();
+ Assertions.checkArgument(length <= 4);
+ int result = 0;
+ for (int i = 0; i < length; i++) {
+ result <<= 8;
+ result |= string.charAt(i);
+ }
+ return result;
+ }
+
}
diff --git a/library/src/test/java/com/google/android/exoplayer/extractor/mp3/BufferingInputTest.java b/library/src/test/java/com/google/android/exoplayer/extractor/mp3/BufferingInputTest.java
new file mode 100644
index 0000000000..d6668f94c6
--- /dev/null
+++ b/library/src/test/java/com/google/android/exoplayer/extractor/mp3/BufferingInputTest.java
@@ -0,0 +1,223 @@
+/*
+ * 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.mp3;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import com.google.android.exoplayer.extractor.DefaultExtractorInput;
+import com.google.android.exoplayer.extractor.ExtractorInput;
+import com.google.android.exoplayer.extractor.TrackOutput;
+import com.google.android.exoplayer.testutil.FakeDataSource;
+import com.google.android.exoplayer.testutil.Util;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.util.ParsableByteArray;
+
+import android.net.Uri;
+import android.test.InstrumentationTestCase;
+
+import org.mockito.Mock;
+
+import java.nio.BufferOverflowException;
+import java.util.Arrays;
+
+/**
+ * Tests for {@link BufferingInput}.
+ */
+public class BufferingInputTest extends InstrumentationTestCase {
+
+ private static final String TEST_URI = "http://www.google.com";
+ private static final byte[] STREAM_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+
+ private ExtractorInput fakeExtractorInput;
+
+ /** Used for verifying interactions. */
+ @Mock private ExtractorInput mockExtractorInput;
+ @Mock private TrackOutput mockTrackOutput;
+
+ @Override
+ public void setUp() throws Exception {
+ Util.setUpMockito(this);
+
+ FakeDataSource.Builder builder = new FakeDataSource.Builder();
+ builder.appendReadData(STREAM_DATA);
+ FakeDataSource fakeDataSource = builder.build();
+ fakeDataSource.open(new DataSpec(Uri.parse(TEST_URI)));
+ fakeExtractorInput = new DefaultExtractorInput(fakeDataSource, 0, STREAM_DATA.length);
+ }
+
+ public void testReadFromExtractor() throws Exception {
+ BufferingInput input = new BufferingInput(5);
+ byte[] target = new byte[4];
+ input.read(fakeExtractorInput, target, 0, 4);
+ assertMatchesStreamData(target, 0, 4);
+ }
+
+ public void testReadCapacityFromExtractor() throws Exception {
+ BufferingInput input = new BufferingInput(5);
+ byte[] target = new byte[5];
+ input.read(fakeExtractorInput, target, 0, 5);
+ assertMatchesStreamData(target, 0, 5);
+ }
+
+ public void testReadOverCapacityFromExtractorFails() throws Exception {
+ BufferingInput input = new BufferingInput(5);
+ byte[] target = new byte[6];
+ try {
+ input.read(fakeExtractorInput, target, 0, 6);
+ fail();
+ } catch (BufferOverflowException e) {
+ // Expected.
+ }
+ }
+
+ public void testReadFromBuffer() throws Exception {
+ BufferingInput input = new BufferingInput(5);
+ byte[] target = new byte[5];
+ input.read(fakeExtractorInput, target, 0, 5);
+
+ // When reading already-buffered data
+ input.returnToMark();
+ input.read(mockExtractorInput, target, 0, 5);
+ assertMatchesStreamData(target, 0, 5);
+
+ // There is no interaction with the extractor input.
+ verifyZeroInteractions(mockExtractorInput);
+ }
+
+ public void testReadFromBufferPartially() throws Exception {
+ BufferingInput input = new BufferingInput(5);
+ byte[] target = new byte[5];
+ input.read(fakeExtractorInput, target, 0, 5);
+
+ // When reading already-buffered data
+ input.returnToMark();
+ input.read(mockExtractorInput, target, 0, 4);
+ assertMatchesStreamData(target, 0, 4);
+
+ // There is no interaction with the extractor input.
+ verifyZeroInteractions(mockExtractorInput);
+ }
+
+ public void testResetDiscardsData() throws Exception {
+ BufferingInput input = new BufferingInput(5);
+ byte[] target = new byte[5];
+ input.read(fakeExtractorInput, target, 0, 5);
+
+ // When the buffer is reset
+ input.reset();
+
+ // Then it is possible to read up to the capacity again.
+ input.read(fakeExtractorInput, target, 0, 5);
+ assertMatchesStreamData(target, 5, 5);
+ }
+
+ public void testGetAvailableByteCountAtWritePosition() throws Exception {
+ BufferingInput input = new BufferingInput(5);
+ byte[] target = new byte[5];
+ input.read(fakeExtractorInput, target, 0, 5);
+ assertEquals(0, input.getAvailableByteCount());
+ }
+
+ public void testGetAvailableByteCountBeforeWritePosition() throws Exception {
+ BufferingInput input = new BufferingInput(5);
+ byte[] target = new byte[5];
+ input.read(fakeExtractorInput, target, 0, 3);
+ input.mark();
+ input.read(fakeExtractorInput, target, 0, 3);
+ input.mark();
+ input.read(fakeExtractorInput, target, 0, 2);
+ input.returnToMark();
+
+ // The reading position is calculated correctly.
+ assertEquals(2, input.getAvailableByteCount());
+ assertEquals(8, fakeExtractorInput.getPosition());
+ }
+
+ public void testGetParsableByteArray() throws Exception {
+ BufferingInput input = new BufferingInput(5);
+ input.skip(fakeExtractorInput, 4);
+ input.mark();
+ input.skip(fakeExtractorInput, 3);
+ input.returnToMark();
+ ParsableByteArray parsableByteArray = input.getParsableByteArray(fakeExtractorInput, 4);
+
+ // The returned array matches the input's internal buffer.
+ assertMatchesStreamData(parsableByteArray.data, 0, 7);
+ }
+
+ public void testGetParsableByteArrayPastCapacity() throws Exception {
+ BufferingInput input = new BufferingInput(5);
+ input.skip(fakeExtractorInput, 4);
+ input.mark();
+ input.skip(fakeExtractorInput, 3);
+ input.mark();
+ input.skip(fakeExtractorInput, 1);
+ input.returnToMark();
+ ParsableByteArray parsableByteArray = input.getParsableByteArray(fakeExtractorInput, 2);
+
+ // The second call to mark() copied the buffer data to the start.
+ assertMatchesStreamData(parsableByteArray.data, 7, 2);
+ }
+
+ public void testDrainEntireBuffer() throws Exception {
+ BufferingInput input = new BufferingInput(5);
+ input.skip(fakeExtractorInput, 3);
+ input.returnToMark();
+
+ // When draining the first three bytes
+ input.drainToOutput(mockTrackOutput, 3);
+
+ // They are appended as sample data.
+ verify(mockTrackOutput).sampleData(any(ParsableByteArray.class), eq(3));
+ }
+
+ public void testDrainTwice() throws Exception {
+ BufferingInput input = new BufferingInput(5);
+ input.skip(fakeExtractorInput, 3);
+ input.returnToMark();
+
+ // When draining one then two bytes
+ input.drainToOutput(mockTrackOutput, 1);
+ assertEquals(2, input.drainToOutput(mockTrackOutput, 3));
+
+ // They are appended as sample data.
+ verify(mockTrackOutput).sampleData(any(ParsableByteArray.class), eq(1));
+ verify(mockTrackOutput).sampleData(any(ParsableByteArray.class), eq(2));
+ }
+
+ public void testDrainPastCapacity() throws Exception {
+ BufferingInput input = new BufferingInput(5);
+ input.skip(fakeExtractorInput, 4);
+ input.mark();
+ input.skip(fakeExtractorInput, 5);
+ input.returnToMark();
+
+ // When draining the entire buffer
+ input.drainToOutput(mockTrackOutput, 5);
+
+ // The sample data is appended as one whole buffer.
+ verify(mockTrackOutput).sampleData(any(ParsableByteArray.class), eq(5));
+ }
+
+ private static void assertMatchesStreamData(byte[] read, int offset, int length) {
+ assertTrue(Arrays.equals(Arrays.copyOfRange(STREAM_DATA, offset, offset + length),
+ Arrays.copyOfRange(read, 0, length)));
+ }
+
+}
diff --git a/library/src/test/java/com/google/android/exoplayer/testutil/Util.java b/library/src/test/java/com/google/android/exoplayer/testutil/Util.java
index 5f86b51829..4d2897e7a4 100644
--- a/library/src/test/java/com/google/android/exoplayer/testutil/Util.java
+++ b/library/src/test/java/com/google/android/exoplayer/testutil/Util.java
@@ -15,6 +15,10 @@
*/
package com.google.android.exoplayer.testutil;
+import android.test.InstrumentationTestCase;
+
+import org.mockito.MockitoAnnotations;
+
import java.util.Random;
/**
@@ -35,4 +39,11 @@ public class Util {
return source;
}
+ public static void setUpMockito(InstrumentationTestCase instrumentationTestCase) {
+ // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2.
+ System.setProperty("dexmaker.dexcache",
+ instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath());
+ MockitoAnnotations.initMocks(instrumentationTestCase);
+ }
+
}