From c5cf9090fc44f9502c97e404f4e185d84a44c5bc Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 2 Jun 2017 09:48:58 -0700 Subject: [PATCH] Assume CBR for MP3s with Info headers Issue: #2895 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157841519 --- .../extractor/mp3/Mp3Extractor.java | 118 +++++++++++------- 1 file changed, 73 insertions(+), 45 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index b0faad71c0..6e114137f1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -87,10 +87,12 @@ public final class Mp3Extractor implements Extractor { /** * Mask that includes the audio header values that must match between frames. */ - private static final int HEADER_MASK = 0xFFFE0C00; - private static final int XING_HEADER = Util.getIntegerCodeForString("Xing"); - private static final int INFO_HEADER = Util.getIntegerCodeForString("Info"); - private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI"); + private static final int MPEG_AUDIO_HEADER_MASK = 0xFFFE0C00; + + private static final int SEEK_HEADER_XING = Util.getIntegerCodeForString("Xing"); + private static final int SEEK_HEADER_INFO = Util.getIntegerCodeForString("Info"); + private static final int SEEK_HEADER_VBRI = Util.getIntegerCodeForString("VBRI"); + private static final int SEEK_HEADER_UNSET = 0; @Flags private final int flags; private final long forcedFirstSampleTimestampUs; @@ -178,7 +180,11 @@ public final class Mp3Extractor implements Extractor { } } if (seeker == null) { - seeker = setupSeeker(input); + seeker = maybeReadSeekFrame(input); + if (seeker == null + || (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { + seeker = getConstantBitrateSeeker(input); + } extractorOutput.seekMap(seeker); trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, @@ -197,7 +203,7 @@ public final class Mp3Extractor implements Extractor { } scratch.setPosition(0); int sampleHeaderData = scratch.readInt(); - if ((sampleHeaderData & HEADER_MASK) != (synchronizedHeaderData & HEADER_MASK) + if (!headersMatch(sampleHeaderData, synchronizedHeaderData) || MpegAudioHeader.getFrameSize(sampleHeaderData) == C.LENGTH_UNSET) { // We have lost synchronization, so attempt to resynchronize starting at the next byte. extractorInput.skipFully(1); @@ -254,7 +260,7 @@ public final class Mp3Extractor implements Extractor { int headerData = scratch.readInt(); int frameSize; if ((candidateSynchronizedHeaderData != 0 - && (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK)) + && !headersMatch(headerData, candidateSynchronizedHeaderData)) || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == C.LENGTH_UNSET) { // The header doesn't match the candidate header or is invalid. Try the next byte offset. if (searchedBytes++ == searchLimitBytes) { @@ -337,37 +343,27 @@ public final class Mp3Extractor implements Extractor { } /** - * Returns a {@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. + * Consumes the next frame from the {@code input} if it contains VBRI or Xing seeking metadata, + * returning a {@link Seeker} if the metadata was present and valid, or {@code null} otherwise. + * After this method returns, the input position is the start of the first frame of audio. * * @param input The {@link ExtractorInput} from which to read. + * @return A {@link Seeker} if seeking metadata was present and valid, or {@code null} otherwise. * @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. - * @return a {@link Seeker}. */ - private Seeker setupSeeker(ExtractorInput input) throws IOException, InterruptedException { - // Read the first frame which may contain a Xing or VBRI header with seeking metadata. + private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException, InterruptedException { ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize); input.peekFully(frame.data, 0, synchronizedHeader.frameSize); - - long position = input.getPosition(); - long length = input.getLength(); - int headerData = 0; - Seeker seeker = null; - - // Check if there is a Xing header. int xingBase = (synchronizedHeader.version & 1) != 0 ? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1 : (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5 - if (frame.limit() >= xingBase + 4) { - frame.setPosition(xingBase); - headerData = frame.readInt(); - } - if (headerData == XING_HEADER || headerData == INFO_HEADER) { - seeker = XingSeeker.create(synchronizedHeader, frame, position, length); + int seekHeader = getSeekFrameHeader(frame, xingBase); + Seeker seeker; + if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) { + seeker = XingSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength()); if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { // If there is a Xing header, read gapless playback metadata at a fixed offset. input.resetPeekPosition(); @@ -377,28 +373,60 @@ public final class Mp3Extractor implements Extractor { gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24()); } input.skipFully(synchronizedHeader.frameSize); - } else if (frame.limit() >= 40) { - // Check if there is a VBRI header. - frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes. - headerData = frame.readInt(); - if (headerData == VBRI_HEADER) { - seeker = VbriSeeker.create(synchronizedHeader, frame, position, length); - input.skipFully(synchronizedHeader.frameSize); + if (seeker != null && !seeker.isSeekable() && seekHeader == SEEK_HEADER_INFO) { + // Fall back to constant bitrate seeking for Info headers missing a table of contents. + return getConstantBitrateSeeker(input); + } + } else if (seekHeader == SEEK_HEADER_VBRI) { + seeker = VbriSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength()); + input.skipFully(synchronizedHeader.frameSize); + } else { // seekerHeader == SEEK_HEADER_UNSET + // This frame doesn't contain seeking information, so reset the peek position. + seeker = null; + input.resetPeekPosition(); + } + return seeker; + } + + /** + * Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate. + */ + private Seeker getConstantBitrateSeeker(ExtractorInput input) + throws IOException, InterruptedException { + input.peekFully(scratch.data, 0, 4); + scratch.setPosition(0); + MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); + return new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, + input.getLength()); + } + + /** + * Returns whether the headers match in those bits masked by {@link #MPEG_AUDIO_HEADER_MASK}. + */ + private static boolean headersMatch(int headerA, long headerB) { + return (headerA & MPEG_AUDIO_HEADER_MASK) == (headerB & MPEG_AUDIO_HEADER_MASK); + } + + /** + * Returns {@link #SEEK_HEADER_XING}, {@link #SEEK_HEADER_INFO} or {@link #SEEK_HEADER_VBRI} if + * the provided {@code frame} may have seeking metadata, or {@link #SEEK_HEADER_UNSET} otherwise. + * If seeking metadata is present, {@code frame}'s position is advanced past the header. + */ + private static int getSeekFrameHeader(ParsableByteArray frame, int xingBase) { + if (frame.limit() >= xingBase + 4) { + frame.setPosition(xingBase); + int headerData = frame.readInt(); + if (headerData == SEEK_HEADER_XING || headerData == SEEK_HEADER_INFO) { + return headerData; } } - - if (seeker == null || (!seeker.isSeekable() - && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { - // Repopulate the synchronized header in case we had to skip an invalid seeking header, which - // would give an invalid CBR bitrate. - input.resetPeekPosition(); - input.peekFully(scratch.data, 0, 4); - scratch.setPosition(0); - MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); - seeker = new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, length); + if (frame.limit() >= 40) { + frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes. + if (frame.readInt() == SEEK_HEADER_VBRI) { + return SEEK_HEADER_VBRI; + } } - - return seeker; + return SEEK_HEADER_UNSET; } /**