Commit 25fb2a82 by andrewlewis Committed by Oliver Woodman

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
parent e6637c50
...@@ -52,7 +52,12 @@ public interface Extractor { ...@@ -52,7 +52,12 @@ public interface Extractor {
/** /**
* Returns whether this extractor can extract samples from the {@link ExtractorInput}, which must * Returns whether this extractor can extract samples from the {@link ExtractorInput}, which must
* provide data from the start of the stream. * 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 IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted. * @throws InterruptedException If the thread was interrupted.
*/ */
......
...@@ -36,12 +36,18 @@ import java.io.IOException; ...@@ -36,12 +36,18 @@ import java.io.IOException;
*/ */
public final class Mp3Extractor implements Extractor { 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; 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 HEADER_MASK = 0xFFFE0C00;
private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); private static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
private static final int XING_HEADER = Util.getIntegerCodeForString("Xing"); private static final int XING_HEADER = Util.getIntegerCodeForString("Xing");
...@@ -85,61 +91,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -85,61 +91,7 @@ public final class Mp3Extractor implements Extractor {
@Override @Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
ParsableByteArray scratch = new ParsableByteArray(4); return synchronize(input, true);
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);
}
} }
@Override @Override
...@@ -158,20 +110,24 @@ public final class Mp3Extractor implements Extractor { ...@@ -158,20 +110,24 @@ public final class Mp3Extractor implements Extractor {
} }
@Override @Override
public int read(ExtractorInput extractorInput, PositionHolder seekPosition) public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (synchronizedHeaderData == 0 if (synchronizedHeaderData == 0 && !synchronizeCatchingEndOfInput(input)) {
&& synchronizeCatchingEndOfInput(extractorInput) == RESULT_END_OF_INPUT) {
return RESULT_END_OF_INPUT; return RESULT_END_OF_INPUT;
} }
if (seeker == null) {
return readSample(extractorInput); 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 { private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
if (sampleBytesRemaining == 0) { if (sampleBytesRemaining == 0) {
long headerPosition = maybeResynchronize(extractorInput); if (!maybeResynchronize(extractorInput)) {
if (headerPosition == RESULT_END_OF_INPUT) {
return RESULT_END_OF_INPUT; return RESULT_END_OF_INPUT;
} }
if (basisTimeUs == -1) { if (basisTimeUs == -1) {
...@@ -201,11 +157,11 @@ public final class Mp3Extractor implements Extractor { ...@@ -201,11 +157,11 @@ public final class Mp3Extractor implements Extractor {
/** /**
* Attempts to read an MPEG audio header at the current offset, resynchronizing if necessary. * 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 { throws IOException, InterruptedException {
extractorInput.resetPeekPosition(); extractorInput.resetPeekPosition();
if (!extractorInput.peekFully(scratch.data, 0, 4, true)) { if (!extractorInput.peekFully(scratch.data, 0, 4, true)) {
return RESULT_END_OF_INPUT; return false;
} }
scratch.setPosition(0); scratch.setPosition(0);
...@@ -214,7 +170,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -214,7 +170,7 @@ public final class Mp3Extractor implements Extractor {
int frameSize = MpegAudioHeader.getFrameSize(sampleHeaderData); int frameSize = MpegAudioHeader.getFrameSize(sampleHeaderData);
if (frameSize != -1) { if (frameSize != -1) {
MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader); MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader);
return RESULT_CONTINUE; return true;
} }
} }
...@@ -223,130 +179,120 @@ public final class Mp3Extractor implements Extractor { ...@@ -223,130 +179,120 @@ public final class Mp3Extractor implements Extractor {
return synchronizeCatchingEndOfInput(extractorInput); return synchronizeCatchingEndOfInput(extractorInput);
} }
private long synchronizeCatchingEndOfInput(ExtractorInput extractorInput) private boolean synchronizeCatchingEndOfInput(ExtractorInput input)
throws IOException, InterruptedException { throws IOException, InterruptedException {
// An EOFException will be raised if any peek 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 peek 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(input, false);
} catch (EOFException e) { } catch (EOFException e) {
return RESULT_END_OF_INPUT; return false;
} }
} }
private long synchronize(ExtractorInput extractorInput) throws IOException, InterruptedException { private boolean synchronize(ExtractorInput input, boolean sniffing)
// TODO: Deduplicate with sniff(). throws IOException, InterruptedException {
extractorInput.resetPeekPosition(); input.resetPeekPosition();
long startPosition = extractorInput.getPosition(); int peekedId3Bytes = 0;
if (startPosition == 0) { if (input.getPosition() == 0) {
// Skip any ID3 headers at the start of the file. // Skip any ID3 headers at the start of the file.
while (true) { while (true) {
extractorInput.peekFully(scratch.data, 0, 3); input.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 + 2 + 1); // "ID3", version, flags input.advancePeekPosition(2 + 1); // version, flags
extractorInput.readFully(scratch.data, 0, 4); input.peekFully(scratch.data, 0, 4);
int headerLength = ((scratch.data[0] & 0x7F) << 21) | ((scratch.data[1] & 0x7F) << 14) scratch.setPosition(0);
| ((scratch.data[2] & 0x7F) << 7) | (scratch.data[3] & 0x7F); int headerLength = scratch.readSynchSafeInt();
extractorInput.skipFully(headerLength); input.advancePeekPosition(headerLength);
startPosition = extractorInput.getPosition(); peekedId3Bytes += 10 + headerLength;
} }
extractorInput.resetPeekPosition(); input.resetPeekPosition();
input.advancePeekPosition(peekedId3Bytes);
} }
// Try to find four consecutive valid MPEG audio frames. // For sniffing, limit the search range to the length of an audio frame after any ID3 metadata.
long headerPosition = startPosition; int searched = 0;
int validFrameCount = 0; int validFrameCount = 0;
int candidateSynchronizedHeaderData = 0; int candidateSynchronizedHeaderData = 0;
while (true) { while (true) {
if (headerPosition - startPosition >= MAX_SYNC_BYTES) { if (sniffing && searched == MAX_SNIFF_BYTES) {
throw new ParserException("Searched too many bytes while resynchronizing."); return false;
} }
if (!sniffing && searched == MAX_SYNC_BYTES) {
if (!extractorInput.peekFully(scratch.data, 0, 4, true)) { throw new ParserException("Searched too many bytes.");
return RESULT_END_OF_INPUT; }
if (!input.peekFully(scratch.data, 0, 4, true)) {
return false;
} }
scratch.setPosition(0); scratch.setPosition(0);
int headerData = scratch.readInt(); int headerData = scratch.readInt();
int frameSize; int frameSize;
if ((candidateSynchronizedHeaderData != 0 if ((candidateSynchronizedHeaderData != 0
&& (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK)) && (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK))
|| (frameSize = MpegAudioHeader.getFrameSize(headerData)) == -1) { || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == -1) {
// The header is invalid or doesn't match the candidate header. Try the next byte offset.
validFrameCount = 0; validFrameCount = 0;
candidateSynchronizedHeaderData = 0; candidateSynchronizedHeaderData = 0;
searched++;
// Try reading a header starting at the next byte. if (sniffing) {
extractorInput.skipFully(1); input.resetPeekPosition();
headerPosition++; input.advancePeekPosition(peekedId3Bytes + searched);
continue; } else {
} input.skipFully(1);
}
if (validFrameCount == 0) { } else {
MpegAudioHeader.populateHeader(headerData, synchronizedHeader); // The header is valid and matches the candidate header.
candidateSynchronizedHeaderData = headerData; validFrameCount++;
} if (validFrameCount == 1) {
MpegAudioHeader.populateHeader(headerData, synchronizedHeader);
// The header was valid and matching (if appropriate). Check another or end synchronization. candidateSynchronizedHeaderData = headerData;
validFrameCount++; } else if (validFrameCount == 4) {
if (validFrameCount == 4) { break;
break; }
input.advancePeekPosition(frameSize - 4);
} }
// Look for more headers.
extractorInput.advancePeekPosition(frameSize - 4);
} }
// Prepare to read the synchronized frame.
// The read position is now synchronized. if (sniffing) {
extractorInput.resetPeekPosition(); input.skipFully(peekedId3Bytes + searched);
synchronizedHeaderData = candidateSynchronizedHeaderData; } else {
if (seeker == null) { input.resetPeekPosition();
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));
} }
synchronizedHeaderData = candidateSynchronizedHeaderData;
return headerPosition; return true;
} }
/** /**
* Sets {@link #seeker} to seek using metadata read from {@code extractorInput}, which should * Sets {@link #seeker} to seek using metadata read from {@code input}, which should provide data
* provide data from the start of the first frame in the stream. On returning, the input's * from the start of the first frame in the stream. On returning, the input's position will be set
* position will be set to the start of the first frame of audio. * to the start of the first frame of audio.
* *
* @param extractorInput The {@link ExtractorInput} from which to read. * @param input The {@link ExtractorInput} from which to read.
* @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 peeked 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 peeked during synchronization. * the next two frames were already peeked during synchronization.
*/ */
private void setupSeeker(ExtractorInput extractorInput, long headerPosition) private void setupSeeker(ExtractorInput input) throws IOException, InterruptedException {
throws IOException, InterruptedException {
// Try to set up seeking based on a Xing or VBRI header.
ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize); ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize);
extractorInput.peekFully(frame.data, 0, synchronizedHeader.frameSize); input.peekFully(frame.data, 0, synchronizedHeader.frameSize);
if (parseSeekerFrame(frame, headerPosition, extractorInput.getLength())) { if (parseSeekerFrame(frame, input.getPosition(), input.getLength())) {
extractorInput.skipFully(synchronizedHeader.frameSize); input.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 use // 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 // an invalid bitrate for CBR seeking.
// already read during synchronization. input.peekFully(scratch.data, 0, 4);
headerPosition += synchronizedHeader.frameSize;
extractorInput.peekFully(scratch.data, 0, 4);
scratch.setPosition(0); scratch.setPosition(0);
MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
} }
seeker = new ConstantBitrateSeeker(headerPosition, synchronizedHeader.bitrate * 1000, seeker = new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate * 1000,
extractorInput.getLength()); input.getLength());
} }
/** /**
...@@ -356,9 +302,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -356,9 +302,7 @@ public final class Mp3Extractor implements Extractor {
* {@code true} and assigns {@link #seeker}. * {@code true} and assigns {@link #seeker}.
*/ */
private boolean parseSeekerFrame(ParsableByteArray frame, long headerPosition, long inputLength) { 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; int xingBase;
if ((synchronizedHeader.version & 1) == 1) { if ((synchronizedHeader.version & 1) == 1) {
// MPEG 1. // MPEG 1.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment