mirror of
https://github.com/samsonjs/media.git
synced 2026-03-30 10:15:48 +00:00
Add new style mp3 extractor.
This commit is contained in:
parent
4a1fed9e86
commit
7d8141e419
9 changed files with 1270 additions and 0 deletions
|
|
@ -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.
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.<byte[]>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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue