mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Using ExtractorInput.peek* instead of BufferingInput.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=111394118
This commit is contained in:
parent
c1e99277d2
commit
7c103ca5f8
3 changed files with 47 additions and 511 deletions
|
|
@ -1,223 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.TestUtil;
|
|
||||||
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 {
|
|
||||||
TestUtil.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)));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -49,7 +49,6 @@ public final class Mp3Extractor implements Extractor {
|
||||||
private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI");
|
private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI");
|
||||||
|
|
||||||
private final long forcedFirstSampleTimestampUs;
|
private final long forcedFirstSampleTimestampUs;
|
||||||
private final BufferingInput inputBuffer;
|
|
||||||
private final ParsableByteArray scratch;
|
private final ParsableByteArray scratch;
|
||||||
private final MpegAudioHeader synchronizedHeader;
|
private final MpegAudioHeader synchronizedHeader;
|
||||||
|
|
||||||
|
|
@ -79,7 +78,6 @@ public final class Mp3Extractor implements Extractor {
|
||||||
*/
|
*/
|
||||||
public Mp3Extractor(long forcedFirstSampleTimestampUs) {
|
public Mp3Extractor(long forcedFirstSampleTimestampUs) {
|
||||||
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
|
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
|
||||||
inputBuffer = new BufferingInput(MpegAudioHeader.MAX_FRAME_SIZE_BYTES * 3);
|
|
||||||
scratch = new ParsableByteArray(4);
|
scratch = new ParsableByteArray(4);
|
||||||
synchronizedHeader = new MpegAudioHeader();
|
synchronizedHeader = new MpegAudioHeader();
|
||||||
basisTimeUs = -1;
|
basisTimeUs = -1;
|
||||||
|
|
@ -157,7 +155,6 @@ public final class Mp3Extractor implements Extractor {
|
||||||
samplesRead = 0;
|
samplesRead = 0;
|
||||||
basisTimeUs = -1;
|
basisTimeUs = -1;
|
||||||
sampleBytesRemaining = 0;
|
sampleBytesRemaining = 0;
|
||||||
inputBuffer.reset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -178,7 +175,7 @@ public final class Mp3Extractor implements Extractor {
|
||||||
return RESULT_END_OF_INPUT;
|
return RESULT_END_OF_INPUT;
|
||||||
}
|
}
|
||||||
if (basisTimeUs == -1) {
|
if (basisTimeUs == -1) {
|
||||||
basisTimeUs = seeker.getTimeUs(getPosition(extractorInput, inputBuffer));
|
basisTimeUs = seeker.getTimeUs(extractorInput.getPosition());
|
||||||
if (forcedFirstSampleTimestampUs != -1) {
|
if (forcedFirstSampleTimestampUs != -1) {
|
||||||
long embeddedFirstSampleTimestampUs = seeker.getTimeUs(0);
|
long embeddedFirstSampleTimestampUs = seeker.getTimeUs(0);
|
||||||
basisTimeUs += forcedFirstSampleTimestampUs - embeddedFirstSampleTimestampUs;
|
basisTimeUs += forcedFirstSampleTimestampUs - embeddedFirstSampleTimestampUs;
|
||||||
|
|
@ -186,24 +183,15 @@ public final class Mp3Extractor implements Extractor {
|
||||||
}
|
}
|
||||||
sampleBytesRemaining = synchronizedHeader.frameSize;
|
sampleBytesRemaining = synchronizedHeader.frameSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
long timeUs = basisTimeUs + (samplesRead * C.MICROS_PER_SECOND / 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();
|
|
||||||
int bytesAppended = trackOutput.sampleData(extractorInput, sampleBytesRemaining, true);
|
int bytesAppended = trackOutput.sampleData(extractorInput, sampleBytesRemaining, true);
|
||||||
if (bytesAppended == C.RESULT_END_OF_INPUT) {
|
if (bytesAppended == C.RESULT_END_OF_INPUT) {
|
||||||
return RESULT_END_OF_INPUT;
|
return RESULT_END_OF_INPUT;
|
||||||
}
|
}
|
||||||
sampleBytesRemaining -= bytesAppended;
|
sampleBytesRemaining -= bytesAppended;
|
||||||
// Return if we still need more data.
|
|
||||||
if (sampleBytesRemaining > 0) {
|
if (sampleBytesRemaining > 0) {
|
||||||
return RESULT_CONTINUE;
|
return RESULT_CONTINUE;
|
||||||
}
|
}
|
||||||
}
|
long timeUs = basisTimeUs + (samplesRead * C.MICROS_PER_SECOND / synchronizedHeader.sampleRate);
|
||||||
|
|
||||||
trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, synchronizedHeader.frameSize, 0, null);
|
trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, synchronizedHeader.frameSize, 0, null);
|
||||||
samplesRead += synchronizedHeader.samplesPerFrame;
|
samplesRead += synchronizedHeader.samplesPerFrame;
|
||||||
sampleBytesRemaining = 0;
|
sampleBytesRemaining = 0;
|
||||||
|
|
@ -215,11 +203,10 @@ public final class Mp3Extractor implements Extractor {
|
||||||
*/
|
*/
|
||||||
private long maybeResynchronize(ExtractorInput extractorInput)
|
private long maybeResynchronize(ExtractorInput extractorInput)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
inputBuffer.mark();
|
extractorInput.resetPeekPosition();
|
||||||
if (!inputBuffer.readAllowingEndOfInput(extractorInput, scratch.data, 0, 4)) {
|
if (!extractorInput.peekFully(scratch.data, 0, 4, true)) {
|
||||||
return RESULT_END_OF_INPUT;
|
return RESULT_END_OF_INPUT;
|
||||||
}
|
}
|
||||||
inputBuffer.returnToMark();
|
|
||||||
|
|
||||||
scratch.setPosition(0);
|
scratch.setPosition(0);
|
||||||
int sampleHeaderData = scratch.readInt();
|
int sampleHeaderData = scratch.readInt();
|
||||||
|
|
@ -232,14 +219,14 @@ public final class Mp3Extractor implements Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronizedHeaderData = 0;
|
synchronizedHeaderData = 0;
|
||||||
inputBuffer.skip(extractorInput, 1);
|
extractorInput.skipFully(1);
|
||||||
return synchronizeCatchingEndOfInput(extractorInput);
|
return synchronizeCatchingEndOfInput(extractorInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
private long synchronizeCatchingEndOfInput(ExtractorInput extractorInput)
|
private long synchronizeCatchingEndOfInput(ExtractorInput extractorInput)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
// An EOFException will be raised if any read operation was partially satisfied. If a seek
|
// An EOFException will be raised if any peek 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
|
// operation resulted in reading from within the last frame, we may try to peek past the end of
|
||||||
// the file in a partially-satisfied read operation, so we need to catch the exception.
|
// the file in a partially-satisfied read operation, so we need to catch the exception.
|
||||||
try {
|
try {
|
||||||
return synchronize(extractorInput);
|
return synchronize(extractorInput);
|
||||||
|
|
@ -249,39 +236,28 @@ public final class Mp3Extractor implements Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private long synchronize(ExtractorInput extractorInput) throws IOException, InterruptedException {
|
private long synchronize(ExtractorInput extractorInput) throws IOException, InterruptedException {
|
||||||
// TODO: Use peekFully instead of a buffering input, and deduplicate with sniff().
|
// TODO: Deduplicate with sniff().
|
||||||
if (extractorInput.getPosition() == 0) {
|
extractorInput.resetPeekPosition();
|
||||||
// Before preparation completes, retrying loads from the start, so clear any buffered data.
|
long startPosition = extractorInput.getPosition();
|
||||||
inputBuffer.reset();
|
|
||||||
} else {
|
|
||||||
// After preparation completes, retrying resumes loading from the old position, so return to
|
|
||||||
// the start of buffered data to parse it again.
|
|
||||||
inputBuffer.returnToMark();
|
|
||||||
}
|
|
||||||
|
|
||||||
long startPosition = getPosition(extractorInput, inputBuffer);
|
|
||||||
|
|
||||||
// Skip any ID3 header at the start of the file.
|
|
||||||
if (startPosition == 0) {
|
if (startPosition == 0) {
|
||||||
|
// Skip any ID3 headers at the start of the file.
|
||||||
while (true) {
|
while (true) {
|
||||||
inputBuffer.read(extractorInput, scratch.data, 0, 3);
|
extractorInput.peekFully(scratch.data, 0, 3);
|
||||||
scratch.setPosition(0);
|
scratch.setPosition(0);
|
||||||
if (scratch.readUnsignedInt24() != ID3_TAG) {
|
if (scratch.readUnsignedInt24() != ID3_TAG) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
extractorInput.skipFully(3);
|
extractorInput.skipFully(3 + 2 + 1); // "ID3", version, flags
|
||||||
extractorInput.readFully(scratch.data, 0, 4);
|
extractorInput.readFully(scratch.data, 0, 4);
|
||||||
int headerLength = ((scratch.data[0] & 0x7F) << 21) | ((scratch.data[1] & 0x7F) << 14)
|
int headerLength = ((scratch.data[0] & 0x7F) << 21) | ((scratch.data[1] & 0x7F) << 14)
|
||||||
| ((scratch.data[2] & 0x7F) << 7) | (scratch.data[3] & 0x7F);
|
| ((scratch.data[2] & 0x7F) << 7) | (scratch.data[3] & 0x7F);
|
||||||
extractorInput.skipFully(headerLength);
|
extractorInput.skipFully(headerLength);
|
||||||
inputBuffer.reset();
|
startPosition = extractorInput.getPosition();
|
||||||
startPosition = getPosition(extractorInput, inputBuffer);
|
|
||||||
}
|
}
|
||||||
inputBuffer.returnToMark();
|
extractorInput.resetPeekPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find four consecutive valid MPEG audio frames.
|
// Try to find four consecutive valid MPEG audio frames.
|
||||||
inputBuffer.mark();
|
|
||||||
long headerPosition = startPosition;
|
long headerPosition = startPosition;
|
||||||
int validFrameCount = 0;
|
int validFrameCount = 0;
|
||||||
int candidateSynchronizedHeaderData = 0;
|
int candidateSynchronizedHeaderData = 0;
|
||||||
|
|
@ -290,7 +266,7 @@ public final class Mp3Extractor implements Extractor {
|
||||||
throw new ParserException("Searched too many bytes while resynchronizing.");
|
throw new ParserException("Searched too many bytes while resynchronizing.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inputBuffer.readAllowingEndOfInput(extractorInput, scratch.data, 0, 4)) {
|
if (!extractorInput.peekFully(scratch.data, 0, 4, true)) {
|
||||||
return RESULT_END_OF_INPUT;
|
return RESULT_END_OF_INPUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,9 +280,7 @@ public final class Mp3Extractor implements Extractor {
|
||||||
candidateSynchronizedHeaderData = 0;
|
candidateSynchronizedHeaderData = 0;
|
||||||
|
|
||||||
// Try reading a header starting at the next byte.
|
// Try reading a header starting at the next byte.
|
||||||
inputBuffer.returnToMark();
|
extractorInput.skipFully(1);
|
||||||
inputBuffer.skip(extractorInput, 1);
|
|
||||||
inputBuffer.mark();
|
|
||||||
headerPosition++;
|
headerPosition++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -323,11 +297,11 @@ public final class Mp3Extractor implements Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for more headers.
|
// Look for more headers.
|
||||||
inputBuffer.skip(extractorInput, frameSize - 4);
|
extractorInput.advancePeekPosition(frameSize - 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The input buffer read position is now synchronized.
|
// The read position is now synchronized.
|
||||||
inputBuffer.returnToMark();
|
extractorInput.resetPeekPosition();
|
||||||
synchronizedHeaderData = candidateSynchronizedHeaderData;
|
synchronizedHeaderData = candidateSynchronizedHeaderData;
|
||||||
if (seeker == null) {
|
if (seeker == null) {
|
||||||
setupSeeker(extractorInput, headerPosition);
|
setupSeeker(extractorInput, headerPosition);
|
||||||
|
|
@ -341,55 +315,48 @@ public final class Mp3Extractor implements Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets {@link #seeker} to seek using metadata from {@link #inputBuffer}, which should have its
|
* Sets {@link #seeker} to seek using metadata read from {@code extractorInput}, which should
|
||||||
* position set to the start of the first frame in the stream. On returning,
|
* provide data from the start of the first frame in the stream. On returning, the input's
|
||||||
* {@link #inputBuffer}'s position and mark will be set to the start of the first frame of audio.
|
* position will be set to the start of the first frame of audio.
|
||||||
*
|
*
|
||||||
* @param extractorInput Source of data for {@link #inputBuffer}.
|
* @param extractorInput The {@link ExtractorInput} from which to read.
|
||||||
* @param headerPosition Position (byte offset) of the synchronized header in the stream.
|
* @param headerPosition The position (byte offset) of the synchronized header in the stream.
|
||||||
* @throws IOException Thrown if there was an error reading from the stream. Not expected if the
|
* @throws IOException Thrown if there was an error reading from the stream. Not expected if the
|
||||||
* next two frames were already read during synchronization.
|
* next two frames were already peeked during synchronization.
|
||||||
* @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if
|
* @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if
|
||||||
* the next two frames were already read during synchronization.
|
* the next two frames were already peeked during synchronization.
|
||||||
*/
|
*/
|
||||||
private void setupSeeker(ExtractorInput extractorInput, long headerPosition)
|
private void setupSeeker(ExtractorInput extractorInput, long headerPosition)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
// Try to set up seeking based on a XING or VBRI header.
|
// Try to set up seeking based on a XING or VBRI header.
|
||||||
if (parseSeekerFrame(extractorInput, headerPosition, extractorInput.getLength())) {
|
ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize);
|
||||||
// Discard the parsed header so we start reading from the first audio frame.
|
extractorInput.peekFully(frame.data, 0, synchronizedHeader.frameSize);
|
||||||
inputBuffer.mark();
|
if (parseSeekerFrame(frame, headerPosition, extractorInput.getLength())) {
|
||||||
|
extractorInput.skipFully(synchronizedHeader.frameSize);
|
||||||
if (seeker != null) {
|
if (seeker != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there was a header but it was not usable, synchronize to the next frame so we don't
|
// If there was a header but it was not usable, synchronize to the next frame so we don't use
|
||||||
// use an invalid bitrate for CBR seeking. This read is guaranteed to succeed if the frame was
|
// an invalid bitrate for CBR seeking. Peeking is guaranteed to succeed if the frame was
|
||||||
// already read during synchronization.
|
// already read during synchronization.
|
||||||
inputBuffer.read(extractorInput, scratch.data, 0, 4);
|
|
||||||
scratch.setPosition(0);
|
|
||||||
headerPosition += synchronizedHeader.frameSize;
|
headerPosition += synchronizedHeader.frameSize;
|
||||||
|
extractorInput.peekFully(scratch.data, 0, 4);
|
||||||
|
scratch.setPosition(0);
|
||||||
MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
|
MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
inputBuffer.returnToMark();
|
|
||||||
seeker = new ConstantBitrateSeeker(headerPosition, synchronizedHeader.bitrate * 1000,
|
seeker = new ConstantBitrateSeeker(headerPosition, synchronizedHeader.bitrate * 1000,
|
||||||
extractorInput.getLength());
|
extractorInput.getLength());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consumes the frame at {@link #inputBuffer}'s current position, advancing it to the next frame.
|
* Tries to read seeking metadata from the given {@code frame}. If there is no seeking metadata,
|
||||||
* The mark is not modified. {@link #seeker} will be assigned based on seeking metadata in the
|
* returns {@code false} and sets {@link #seeker} to null. If seeking metadata is present and
|
||||||
* frame. If there is no seeking metadata, returns {@code false} and sets {@link #seeker} to null.
|
* unusable, returns {@code true} and sets {@link #seeker} to null. Otherwise, returns
|
||||||
* If seeking metadata is present and unusable, returns {@code true} and sets {@link #seeker} to
|
* {@code true} and assigns {@link #seeker}.
|
||||||
* null. Otherwise, returns {@code true} and assigns {@link #seeker}.
|
|
||||||
*/
|
*/
|
||||||
private boolean parseSeekerFrame(ExtractorInput extractorInput, long headerPosition,
|
private boolean parseSeekerFrame(ParsableByteArray frame, long headerPosition, long inputLength) {
|
||||||
long inputLength) throws IOException, InterruptedException {
|
|
||||||
// Read the first frame so it can be parsed for seeking metadata.
|
|
||||||
inputBuffer.mark();
|
|
||||||
seeker = null;
|
seeker = null;
|
||||||
ParsableByteArray frame =
|
|
||||||
inputBuffer.getParsableByteArray(extractorInput, synchronizedHeader.frameSize);
|
|
||||||
|
|
||||||
// Check if there is a XING header.
|
// Check if there is a XING header.
|
||||||
int xingBase;
|
int xingBase;
|
||||||
|
|
@ -427,11 +394,6 @@ public final class Mp3Extractor implements Extractor {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link SeekMap} that also allows mapping from position (byte offset) back to time, which can be
|
* {@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.
|
* used to work out the new sample basis timestamp after seeking and resynchronization.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue