Add new style mp3 extractor.

This commit is contained in:
Oliver Woodman 2015-04-11 01:33:31 +01:00
parent 4a1fed9e86
commit 7d8141e419
9 changed files with 1270 additions and 0 deletions

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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)));
}
}

View file

@ -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);
}
}