Merge MP3 sniffing/synchronization functionality.

As part of this change, Extractor.sniff may read/skip (not just peek) if it
returns true. This allows Extractors to avoid parsing the input a second time in
read.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=112683272
This commit is contained in:
andrewlewis 2016-01-21 07:26:27 -08:00 committed by Oliver Woodman
parent e6637c50c2
commit 25fb2a826e
2 changed files with 94 additions and 145 deletions

View file

@ -52,7 +52,12 @@ public interface Extractor {
/**
* Returns whether this extractor can extract samples from the {@link ExtractorInput}, which must
* provide data from the start of the stream.
* <p>
* If {@code true} is returned, the {@code input}'s reading position may have been modified.
* Otherwise, only its peek position may have been modified.
*
* @param input The {@link ExtractorInput} from which data should be peeked/read.
* @return Whether this extractor can read the provided input.
* @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted.
*/

View file

@ -36,12 +36,18 @@ import java.io.IOException;
*/
public final class Mp3Extractor implements Extractor {
/** The maximum number of bytes to search when synchronizing, before giving up. */
/**
* The maximum number of bytes to search when synchronizing, before giving up.
*/
private static final int MAX_SYNC_BYTES = 128 * 1024;
/** The maximum number of bytes to read when sniffing, excluding the header, before giving up. */
private static final int MAX_SNIFF_BYTES = 4 * 1024;
/**
* The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up.
*/
private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;
/** Mask that includes the audio header values that must match between frames. */
/**
* 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 int XING_HEADER = Util.getIntegerCodeForString("Xing");
@ -85,61 +91,7 @@ public final class Mp3Extractor implements Extractor {
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
ParsableByteArray scratch = new ParsableByteArray(4);
int startPosition = 0;
while (true) {
input.peekFully(scratch.data, 0, 3);
scratch.setPosition(0);
if (scratch.readUnsignedInt24() != ID3_TAG) {
break;
}
input.advancePeekPosition(3);
input.peekFully(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);
input.advancePeekPosition(headerLength);
startPosition += 3 + 3 + 4 + headerLength;
}
input.resetPeekPosition();
input.advancePeekPosition(startPosition);
// Try to find four consecutive valid MPEG audio frames.
int headerPosition = startPosition;
int validFrameCount = 0;
int candidateSynchronizedHeaderData = 0;
while (true) {
if (headerPosition - startPosition >= MAX_SNIFF_BYTES) {
return false;
}
input.peekFully(scratch.data, 0, 4);
scratch.setPosition(0);
int headerData = scratch.readInt();
int frameSize;
if ((candidateSynchronizedHeaderData != 0
&& (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK))
|| (frameSize = MpegAudioHeader.getFrameSize(headerData)) == -1) {
validFrameCount = 0;
candidateSynchronizedHeaderData = 0;
// Try reading a header starting at the next byte.
input.resetPeekPosition();
input.advancePeekPosition(++headerPosition);
continue;
}
if (validFrameCount == 0) {
candidateSynchronizedHeaderData = headerData;
}
// The header was valid and matching (if appropriate). Check another or end synchronization.
if (++validFrameCount == 4) {
return true;
}
// Look for more headers.
input.advancePeekPosition(frameSize - 4);
}
return synchronize(input, true);
}
@Override
@ -158,20 +110,24 @@ public final class Mp3Extractor implements Extractor {
}
@Override
public int read(ExtractorInput extractorInput, PositionHolder seekPosition)
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
if (synchronizedHeaderData == 0
&& synchronizeCatchingEndOfInput(extractorInput) == RESULT_END_OF_INPUT) {
if (synchronizedHeaderData == 0 && !synchronizeCatchingEndOfInput(input)) {
return RESULT_END_OF_INPUT;
}
return readSample(extractorInput);
if (seeker == null) {
setupSeeker(input);
extractorOutput.seekMap(seeker);
trackOutput.format(MediaFormat.createAudioFormat(null, synchronizedHeader.mimeType,
MediaFormat.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, seeker.getDurationUs(),
synchronizedHeader.channels, synchronizedHeader.sampleRate, null, null));
}
return readSample(input);
}
private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
if (sampleBytesRemaining == 0) {
long headerPosition = maybeResynchronize(extractorInput);
if (headerPosition == RESULT_END_OF_INPUT) {
if (!maybeResynchronize(extractorInput)) {
return RESULT_END_OF_INPUT;
}
if (basisTimeUs == -1) {
@ -201,11 +157,11 @@ public final class Mp3Extractor implements Extractor {
/**
* Attempts to read an MPEG audio header at the current offset, resynchronizing if necessary.
*/
private long maybeResynchronize(ExtractorInput extractorInput)
private boolean maybeResynchronize(ExtractorInput extractorInput)
throws IOException, InterruptedException {
extractorInput.resetPeekPosition();
if (!extractorInput.peekFully(scratch.data, 0, 4, true)) {
return RESULT_END_OF_INPUT;
return false;
}
scratch.setPosition(0);
@ -214,7 +170,7 @@ public final class Mp3Extractor implements Extractor {
int frameSize = MpegAudioHeader.getFrameSize(sampleHeaderData);
if (frameSize != -1) {
MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader);
return RESULT_CONTINUE;
return true;
}
}
@ -223,130 +179,120 @@ public final class Mp3Extractor implements Extractor {
return synchronizeCatchingEndOfInput(extractorInput);
}
private long synchronizeCatchingEndOfInput(ExtractorInput extractorInput)
private boolean synchronizeCatchingEndOfInput(ExtractorInput input)
throws IOException, InterruptedException {
// 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 peek past the end of
// the file in a partially-satisfied read operation, so we need to catch the exception.
try {
return synchronize(extractorInput);
return synchronize(input, false);
} catch (EOFException e) {
return RESULT_END_OF_INPUT;
return false;
}
}
private long synchronize(ExtractorInput extractorInput) throws IOException, InterruptedException {
// TODO: Deduplicate with sniff().
extractorInput.resetPeekPosition();
long startPosition = extractorInput.getPosition();
if (startPosition == 0) {
private boolean synchronize(ExtractorInput input, boolean sniffing)
throws IOException, InterruptedException {
input.resetPeekPosition();
int peekedId3Bytes = 0;
if (input.getPosition() == 0) {
// Skip any ID3 headers at the start of the file.
while (true) {
extractorInput.peekFully(scratch.data, 0, 3);
input.peekFully(scratch.data, 0, 3);
scratch.setPosition(0);
if (scratch.readUnsignedInt24() != ID3_TAG) {
break;
}
extractorInput.skipFully(3 + 2 + 1); // "ID3", version, flags
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);
startPosition = extractorInput.getPosition();
input.advancePeekPosition(2 + 1); // version, flags
input.peekFully(scratch.data, 0, 4);
scratch.setPosition(0);
int headerLength = scratch.readSynchSafeInt();
input.advancePeekPosition(headerLength);
peekedId3Bytes += 10 + headerLength;
}
extractorInput.resetPeekPosition();
input.resetPeekPosition();
input.advancePeekPosition(peekedId3Bytes);
}
// Try to find four consecutive valid MPEG audio frames.
long headerPosition = startPosition;
// For sniffing, limit the search range to the length of an audio frame after any ID3 metadata.
int searched = 0;
int validFrameCount = 0;
int candidateSynchronizedHeaderData = 0;
while (true) {
if (headerPosition - startPosition >= MAX_SYNC_BYTES) {
throw new ParserException("Searched too many bytes while resynchronizing.");
if (sniffing && searched == MAX_SNIFF_BYTES) {
return false;
}
if (!extractorInput.peekFully(scratch.data, 0, 4, true)) {
return RESULT_END_OF_INPUT;
if (!sniffing && searched == MAX_SYNC_BYTES) {
throw new ParserException("Searched too many bytes.");
}
if (!input.peekFully(scratch.data, 0, 4, true)) {
return false;
}
scratch.setPosition(0);
int headerData = scratch.readInt();
int frameSize;
if ((candidateSynchronizedHeaderData != 0
&& (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK))
|| (frameSize = MpegAudioHeader.getFrameSize(headerData)) == -1) {
// The header is invalid or doesn't match the candidate header. Try the next byte offset.
validFrameCount = 0;
candidateSynchronizedHeaderData = 0;
// Try reading a header starting at the next byte.
extractorInput.skipFully(1);
headerPosition++;
continue;
searched++;
if (sniffing) {
input.resetPeekPosition();
input.advancePeekPosition(peekedId3Bytes + searched);
} else {
input.skipFully(1);
}
} else {
// The header is valid and matches the candidate header.
validFrameCount++;
if (validFrameCount == 1) {
MpegAudioHeader.populateHeader(headerData, synchronizedHeader);
candidateSynchronizedHeaderData = headerData;
} else if (validFrameCount == 4) {
break;
}
input.advancePeekPosition(frameSize - 4);
}
if (validFrameCount == 0) {
MpegAudioHeader.populateHeader(headerData, synchronizedHeader);
candidateSynchronizedHeaderData = headerData;
}
// The header was valid and matching (if appropriate). Check another or end synchronization.
validFrameCount++;
if (validFrameCount == 4) {
break;
}
// Look for more headers.
extractorInput.advancePeekPosition(frameSize - 4);
}
// The read position is now synchronized.
extractorInput.resetPeekPosition();
// Prepare to read the synchronized frame.
if (sniffing) {
input.skipFully(peekedId3Bytes + searched);
} else {
input.resetPeekPosition();
}
synchronizedHeaderData = candidateSynchronizedHeaderData;
if (seeker == null) {
setupSeeker(extractorInput, headerPosition);
extractorOutput.seekMap(seeker);
trackOutput.format(MediaFormat.createAudioFormat(null, synchronizedHeader.mimeType,
MediaFormat.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, seeker.getDurationUs(),
synchronizedHeader.channels, synchronizedHeader.sampleRate, null, null));
}
return headerPosition;
return true;
}
/**
* Sets {@link #seeker} to seek using metadata read from {@code extractorInput}, which should
* provide data from the start of the first frame in the stream. On returning, the input's
* position will be set to the start of the first frame of audio.
* Sets {@link #seeker} to seek using metadata read from {@code input}, which should provide data
* from the start of the first frame in the stream. On returning, the input's position will be set
* to the start of the first frame of audio.
*
* @param extractorInput The {@link ExtractorInput} from which to read.
* @param headerPosition The position (byte offset) of the synchronized header in the stream.
* @param input The {@link ExtractorInput} from which to read.
* @throws IOException Thrown if there was an error reading from the stream. Not expected if the
* next two frames were already peeked during synchronization.
* @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if
* the next two frames were already peeked during synchronization.
*/
private void setupSeeker(ExtractorInput extractorInput, long headerPosition)
throws IOException, InterruptedException {
// Try to set up seeking based on a Xing or VBRI header.
private void setupSeeker(ExtractorInput input) throws IOException, InterruptedException {
ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize);
extractorInput.peekFully(frame.data, 0, synchronizedHeader.frameSize);
if (parseSeekerFrame(frame, headerPosition, extractorInput.getLength())) {
extractorInput.skipFully(synchronizedHeader.frameSize);
input.peekFully(frame.data, 0, synchronizedHeader.frameSize);
if (parseSeekerFrame(frame, input.getPosition(), input.getLength())) {
input.skipFully(synchronizedHeader.frameSize);
if (seeker != null) {
return;
}
// If there was a header but it was not usable, synchronize to the next frame so we don't use
// an invalid bitrate for CBR seeking. Peeking is guaranteed to succeed if the frame was
// already read during synchronization.
headerPosition += synchronizedHeader.frameSize;
extractorInput.peekFully(scratch.data, 0, 4);
// an invalid bitrate for CBR seeking.
input.peekFully(scratch.data, 0, 4);
scratch.setPosition(0);
MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
}
seeker = new ConstantBitrateSeeker(headerPosition, synchronizedHeader.bitrate * 1000,
extractorInput.getLength());
seeker = new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate * 1000,
input.getLength());
}
/**
@ -356,9 +302,7 @@ public final class Mp3Extractor implements Extractor {
* {@code true} and assigns {@link #seeker}.
*/
private boolean parseSeekerFrame(ParsableByteArray frame, long headerPosition, long inputLength) {
seeker = null;
// Check if there is a XING header.
// Check if there is a Xing header.
int xingBase;
if ((synchronizedHeader.version & 1) == 1) {
// MPEG 1.