diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/Extractor.java index e44e67b3f4..46b70232f6 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/Extractor.java @@ -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. + *

+ * 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. */ diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java index 19f6f311e4..22c6137466 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java @@ -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.