diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Id3Util.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Id3Util.java index a4ad79a85d..06aa5add34 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Id3Util.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Id3Util.java @@ -31,13 +31,6 @@ import java.util.regex.Pattern; */ /* package */ final class Id3Util { - public static final class Metadata { - - public int encoderDelay; - public int encoderPadding; - - } - /** * The maximum valid length for metadata in bytes. */ @@ -54,12 +47,12 @@ import java.util.regex.Pattern; * Peeks data from the input and parses ID3 metadata. * * @param input The {@link ExtractorInput} from which data should be peeked. - * @param out {@link Metadata} to populate based on the input. + * @param out {@link Mp3Extractor.Metadata} to populate based on the input. * @return The number of bytes peeked from the input. * @throws IOException If an error occurred peeking from the input. * @throws InterruptedException If the thread was interrupted. */ - public static int parseId3(ExtractorInput input, Metadata out) + public static int parseId3(ExtractorInput input, Mp3Extractor.Metadata out) throws IOException, InterruptedException { out.encoderDelay = 0; out.encoderPadding = 0; @@ -100,7 +93,8 @@ import java.util.regex.Pattern; && !(majorVersion == 4 && (flags & 0x0F) != 0); } - private static void parseMetadata(ParsableByteArray frame, int version, int flags, Metadata out) { + private static void parseMetadata(ParsableByteArray frame, int version, int flags, + Mp3Extractor.Metadata out) { unescape(frame, version, flags); // Skip any extended header. 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 a5079670bf..e16d294845 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 @@ -56,7 +56,7 @@ public final class Mp3Extractor implements Extractor { private final long forcedFirstSampleTimestampUs; private final ParsableByteArray scratch; private final MpegAudioHeader synchronizedHeader; - private final Id3Util.Metadata metadata; + private final Metadata metadata; // Extractor outputs. private ExtractorOutput extractorOutput; @@ -86,7 +86,7 @@ public final class Mp3Extractor implements Extractor { this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; scratch = new ParsableByteArray(4); synchronizedHeader = new MpegAudioHeader(); - metadata = new Id3Util.Metadata(); + metadata = new Metadata(); basisTimeUs = -1; } @@ -259,64 +259,51 @@ public final class Mp3Extractor implements Extractor { * the next two frames were already peeked during synchronization. */ private void setupSeeker(ExtractorInput input) throws IOException, InterruptedException { + // Read the first frame which may contain a Xing or VBRI header with seeking metadata. ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize); input.peekFully(frame.data, 0, synchronizedHeader.frameSize); - if (parseSeekerFrame(frame, input.getPosition(), input.getLength())) { - input.skipFully(synchronizedHeader.frameSize); - if (seeker != null) { - return; + + long position = input.getPosition(); + long length = input.getLength(); + + // 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 + frame.setPosition(xingBase); + int headerData = frame.readInt(); + if (headerData == XING_HEADER || headerData == INFO_HEADER) { + seeker = XingSeeker.create(synchronizedHeader, frame, position, length); + if (seeker != null && metadata.encoderDelay == 0 && metadata.encoderPadding == 0) { + // If there is a Xing header, read gapless playback metadata at a fixed offset. + input.resetPeekPosition(); + input.advancePeekPosition(xingBase + 141); + input.peekFully(scratch.data, 0, 3); + scratch.setPosition(0); + int gaplessMetadata = scratch.readUnsignedInt24(); + metadata.encoderDelay = gaplessMetadata >> 12; + metadata.encoderPadding = gaplessMetadata & 0x0FFF; } - // 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. + input.skipFully(synchronizedHeader.frameSize); + } else { + // 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); + input.skipFully(synchronizedHeader.frameSize); + } + } + + if (seeker == null) { + // 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); } - seeker = new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate * 1000, - input.getLength()); - } - - /** - * Tries to read seeking metadata from the given {@code frame}. If there is no seeking metadata, - * returns {@code false} and sets {@link #seeker} to null. If seeking metadata is present and - * unusable, returns {@code true} and sets {@link #seeker} to null. Otherwise, returns - * {@code true} and assigns {@link #seeker}. - */ - private boolean parseSeekerFrame(ParsableByteArray frame, long headerPosition, long inputLength) { - // Check if there is a Xing header. - int xingBase; - if ((synchronizedHeader.version & 1) == 1) { - // MPEG 1. - if (synchronizedHeader.channels != 1) { - xingBase = 32; - } else { - xingBase = 17; - } - } else { - // MPEG 2 or 2.5. - if (synchronizedHeader.channels != 1) { - xingBase = 17; - } else { - xingBase = 9; - } - } - frame.setPosition(4 + xingBase); - int headerData = frame.readInt(); - if (headerData == XING_HEADER || headerData == INFO_HEADER) { - seeker = XingSeeker.create(synchronizedHeader, frame, headerPosition, inputLength); - return true; - } - - // 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, headerPosition); - return true; - } - - // Neither header is present. - return false; } /** @@ -338,4 +325,11 @@ public final class Mp3Extractor implements Extractor { } + /* package */ static final class Metadata { + + public int encoderDelay; + public int encoderPadding; + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/util/MpegAudioHeader.java b/library/src/main/java/com/google/android/exoplayer/util/MpegAudioHeader.java index 60e7d248e4..92f2c05db9 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/MpegAudioHeader.java +++ b/library/src/main/java/com/google/android/exoplayer/util/MpegAudioHeader.java @@ -172,7 +172,8 @@ public final class MpegAudioHeader { String mimeType = MIME_TYPE_BY_LAYER[3 - layer]; int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2; - header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame); + header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate * 1000, + samplesPerFrame); return true; } @@ -186,7 +187,7 @@ public final class MpegAudioHeader { public int sampleRate; /** Number of audio channels in the frame. */ public int channels; - /** Bitrate of the frame in kbit/s. */ + /** Bitrate of the frame in bit/s. */ public int bitrate; /** Number of samples stored in the frame. */ public int samplesPerFrame;