mirror of
https://github.com/samsonjs/media.git
synced 2026-03-27 09:45:47 +00:00
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:
parent
e6637c50c2
commit
25fb2a826e
2 changed files with 94 additions and 145 deletions
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue