mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +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
|
* 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;
|
||||||
*/
|
*/
|
||||||
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 {
|
||||||
|
|
||||||
@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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
||||||
/**
|
/**
|
||||||
* 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 {
|
||||||
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 {
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
if (validFrameCount == 0) {
|
// The header is valid and matches the candidate header.
|
||||||
|
validFrameCount++;
|
||||||
|
if (validFrameCount == 1) {
|
||||||
MpegAudioHeader.populateHeader(headerData, synchronizedHeader);
|
MpegAudioHeader.populateHeader(headerData, synchronizedHeader);
|
||||||
candidateSynchronizedHeaderData = headerData;
|
candidateSynchronizedHeaderData = headerData;
|
||||||
}
|
} else if (validFrameCount == 4) {
|
||||||
|
|
||||||
// The header was valid and matching (if appropriate). Check another or end synchronization.
|
|
||||||
validFrameCount++;
|
|
||||||
if (validFrameCount == 4) {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
input.advancePeekPosition(frameSize - 4);
|
||||||
// Look for more headers.
|
}
|
||||||
extractorInput.advancePeekPosition(frameSize - 4);
|
}
|
||||||
|
// Prepare to read the synchronized frame.
|
||||||
|
if (sniffing) {
|
||||||
|
input.skipFully(peekedId3Bytes + searched);
|
||||||
|
} else {
|
||||||
|
input.resetPeekPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The read position is now synchronized.
|
|
||||||
extractorInput.resetPeekPosition();
|
|
||||||
synchronizedHeaderData = candidateSynchronizedHeaderData;
|
synchronizedHeaderData = candidateSynchronizedHeaderData;
|
||||||
if (seeker == null) {
|
return true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {
|
||||||
* {@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.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue