From 828aaaf27b013268850fa3c5d53c3e996b95066c Mon Sep 17 00:00:00 2001 From: tolriq Date: Fri, 22 May 2026 09:20:55 +0200 Subject: [PATCH 1/5] Fix MP3 metadata average bitrates --- .../media3/extractor/mp3/VbriSeeker.java | 28 +++++-- .../media3/extractor/mp3/XingFrame.java | 25 ++++++ .../media3/extractor/mp3/XingSeeker.java | 10 +-- .../media3/extractor/mp3/VbriSeekerTest.java | 81 +++++++++++++++++++ .../media3/extractor/mp3/XingSeekerTest.java | 15 ++++ ...r-vbr-vbri-header-truncated-toc.mp3.0.dump | 2 +- ...r-vbr-vbri-header-truncated-toc.mp3.1.dump | 2 +- ...r-vbr-vbri-header-truncated-toc.mp3.2.dump | 2 +- ...r-vbr-vbri-header-truncated-toc.mp3.3.dump | 2 +- ...ader-truncated-toc.mp3.unknown_length.dump | 2 +- .../mp3/bear-vbr-vbri-header.mp3.0.dump | 2 +- .../mp3/bear-vbr-vbri-header.mp3.1.dump | 2 +- .../mp3/bear-vbr-vbri-header.mp3.2.dump | 2 +- .../mp3/bear-vbr-vbri-header.mp3.3.dump | 2 +- ...ar-vbr-vbri-header.mp3.unknown_length.dump | 2 +- .../bear-vbr-xing-header-no-toc.mp3.0.dump | 2 +- ...xing-header-no-toc.mp3.unknown_length.dump | 2 +- .../mp3/bear-vbr-xing-header.mp3.0.dump | 2 +- .../mp3/bear-vbr-xing-header.mp3.1.dump | 2 +- .../mp3/bear-vbr-xing-header.mp3.2.dump | 2 +- .../mp3/bear-vbr-xing-header.mp3.3.dump | 2 +- ...ar-vbr-xing-header.mp3.unknown_length.dump | 2 +- 22 files changed, 166 insertions(+), 27 deletions(-) create mode 100644 libraries/extractor/src/test/java/androidx/media3/extractor/mp3/VbriSeekerTest.java diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/VbriSeeker.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/VbriSeeker.java index 32619dae5c4..671bb0d8497 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/VbriSeeker.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/VbriSeeker.java @@ -24,6 +24,7 @@ import androidx.media3.common.util.Util; import androidx.media3.extractor.MpegAudioUtil; import androidx.media3.extractor.SeekPoint; +import java.math.RoundingMode; /** MP3 seeker that uses metadata from a VBRI header. */ /* package */ final class VbriSeeker implements Seeker { @@ -107,7 +108,12 @@ public static VbriSeeker create( } return new VbriSeeker( - timesUs, positions, durationUs, startOfMp3Data, endOfMp3Data, mpegAudioHeader.bitrate); + timesUs, + positions, + durationUs, + startOfMp3Data, + endOfMp3Data, + computeAverageBitrate(endOfMp3Data - startOfMp3Data, durationUs)); } private final long[] timesUs; @@ -115,7 +121,7 @@ public static VbriSeeker create( private final long durationUs; private final long dataStartPosition; private final long dataEndPosition; - private final int bitrate; + private final int averageBitrate; private VbriSeeker( long[] timesUs, @@ -123,13 +129,13 @@ private VbriSeeker( long durationUs, long dataStartPosition, long dataEndPosition, - int bitrate) { + int averageBitrate) { this.timesUs = timesUs; this.positions = positions; this.durationUs = durationUs; this.dataStartPosition = dataStartPosition; this.dataEndPosition = dataEndPosition; - this.bitrate = bitrate; + this.averageBitrate = averageBitrate; } @Override @@ -171,6 +177,18 @@ public long getDataEndPosition() { @Override public int getAverageBitrate() { - return bitrate; + return averageBitrate; + } + + private static int computeAverageBitrate(long dataSize, long durationUs) { + if (dataSize <= 0 || durationUs <= 0) { + return C.RATE_UNSET_INT; + } + long averageBitrate = + Util.scaleLargeValue( + dataSize, C.BITS_PER_BYTE * C.MICROS_PER_SECOND, durationUs, RoundingMode.HALF_UP); + return averageBitrate > 0 && averageBitrate <= Integer.MAX_VALUE + ? (int) averageBitrate + : C.RATE_UNSET_INT; } } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingFrame.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingFrame.java index b62e0f074e2..9326fdcb210 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingFrame.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingFrame.java @@ -21,6 +21,7 @@ import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.Util; import androidx.media3.extractor.MpegAudioUtil; +import java.math.RoundingMode; /** Representation of a LAME Xing or Info frame. */ /* package */ final class XingFrame { @@ -155,6 +156,30 @@ public long computeDurationUs() { (frameCount * header.samplesPerFrame) - 1, header.sampleRate); } + /** + * Computes the average bitrate, in bits per second, represented by this frame and {@code + * dataSize}. Returns {@link C#RATE_UNSET_INT} if it can't be computed. + * + * @param dataSize The encoded stream size, including the Xing or Info frame. + */ + public int computeAverageBitrate(long dataSize) { + if (frameCount == C.LENGTH_UNSET + || frameCount == 0 + || dataSize == C.LENGTH_UNSET + || dataSize <= header.frameSize) { + return C.RATE_UNSET_INT; + } + long averageBitrate = + Util.scaleLargeValue( + dataSize - header.frameSize, + C.BITS_PER_BYTE * C.MICROS_PER_SECOND, + computeDurationUs(), + RoundingMode.HALF_UP); + return averageBitrate > 0 && averageBitrate <= Integer.MAX_VALUE + ? (int) averageBitrate + : C.RATE_UNSET_INT; + } + /** Provide the metadata derived from this Xing frame, such as ReplayGain data. */ @Nullable public Metadata getMetadata() { diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingSeeker.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingSeeker.java index e7726ab6458..b39b24e132f 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingSeeker.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingSeeker.java @@ -65,7 +65,7 @@ public static XingSeeker create(XingFrame xingFrame, long position, long streamL position, xingFrame.header.frameSize, durationUs, - xingFrame.header.bitrate, + xingFrame.computeAverageBitrate(dataSize), dataSize, xingFrame.tableOfContents); } @@ -73,7 +73,7 @@ public static XingSeeker create(XingFrame xingFrame, long position, long streamL private final long dataStartPosition; private final int xingFrameSize; private final long durationUs; - private final int bitrate; + private final int averageBitrate; /** Data size, including the XING frame. */ private final long dataSize; @@ -90,13 +90,13 @@ private XingSeeker( long dataStartPosition, int xingFrameSize, long durationUs, - int bitrate, + int averageBitrate, long dataSize, @Nullable long[] tableOfContents) { this.dataStartPosition = dataStartPosition; this.xingFrameSize = xingFrameSize; this.durationUs = durationUs; - this.bitrate = bitrate; + this.averageBitrate = averageBitrate; this.dataSize = dataSize; this.tableOfContents = tableOfContents; dataEndPosition = dataSize == C.LENGTH_UNSET ? C.INDEX_UNSET : dataStartPosition + dataSize; @@ -173,7 +173,7 @@ public long getDataEndPosition() { @Override public int getAverageBitrate() { - return bitrate; + return averageBitrate; } /** diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/VbriSeekerTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/VbriSeekerTest.java new file mode 100644 index 00000000000..b1bdb9fccf4 --- /dev/null +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/VbriSeekerTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.extractor.mp3; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.truth.Truth.assertThat; + +import androidx.media3.common.C; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.Util; +import androidx.media3.extractor.MpegAudioUtil; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.math.RoundingMode; +import java.nio.ByteBuffer; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link VbriSeeker}. */ +@RunWith(AndroidJUnit4.class) +public final class VbriSeekerTest { + + private static final int VBRI_FRAME_HEADER_DATA = 0xFFFB3000; + private static final int VBRI_FRAME_POSITION = 157; + + @Test + public void getAverageBitrate_returnsAverageFromDataSizeAndDuration() { + MpegAudioUtil.Header header = new MpegAudioUtil.Header(); + header.setForHeaderData(VBRI_FRAME_HEADER_DATA); + int dataSize = 1_000; + int frameCount = 40; + VbriSeeker seeker = + checkNotNull( + VbriSeeker.create( + /* inputLength= */ C.LENGTH_UNSET, + VBRI_FRAME_POSITION, + header, + createVbriFrame(dataSize, frameCount, /* segmentSizes= */ 400, 600))); + long durationUs = + Util.sampleCountToDurationUs( + ((long) frameCount * header.samplesPerFrame) - 1, header.sampleRate); + int expectedAverageBitrate = + (int) + Util.scaleLargeValue( + dataSize, + C.BITS_PER_BYTE * C.MICROS_PER_SECOND, + durationUs, + RoundingMode.HALF_UP); + + assertThat(seeker.getAverageBitrate()).isEqualTo(expectedAverageBitrate); + assertThat(seeker.getAverageBitrate()).isNotEqualTo(header.bitrate); + } + + private static ParsableByteArray createVbriFrame( + int dataSize, int frameCount, int... segmentSizes) { + ByteBuffer payload = ByteBuffer.allocate(6 + 4 + 4 + 2 + 2 + 2 + 2 + 2 * segmentSizes.length); + payload.position(6); + payload.putInt(dataSize); + payload.putInt(frameCount); + payload.putShort((short) segmentSizes.length); + payload.putShort((short) 1); // scale + payload.putShort((short) 2); // entry size + payload.putShort((short) 0); + for (int segmentSize : segmentSizes) { + payload.putShort((short) segmentSize); + } + return new ParsableByteArray(payload.array()); + } +} diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/XingSeekerTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/XingSeekerTest.java index c71614bc891..3f33c6749e6 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/XingSeekerTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/XingSeekerTest.java @@ -24,6 +24,7 @@ import androidx.media3.extractor.SeekMap.SeekPoints; import androidx.media3.extractor.SeekPoint; import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.math.RoundingMode; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -86,6 +87,20 @@ public void getTimeUsAtEndOfStream() { assertThat(seeker.getTimeUs(XING_AUDIO_END_POSITION)).isEqualTo(XING_STREAM_DURATION_US); } + @Test + public void getAverageBitrate_returnsAverageFromDataSizeAndDuration() { + int expectedAverageBitrate = + (int) + Util.scaleLargeValue( + XING_FRAME.dataSize - XING_FRAME.header.frameSize, + C.BITS_PER_BYTE * C.MICROS_PER_SECOND, + XING_STREAM_DURATION_US, + RoundingMode.HALF_UP); + + assertThat(seeker.getAverageBitrate()).isEqualTo(expectedAverageBitrate); + assertThat(seeker.getAverageBitrate()).isNotEqualTo(XING_FRAME.header.bitrate); + } + // https://github.com/androidx/media/issues/3117#issuecomment-4046538506 @Test public void getTimeUsAtEndOfStream_xingLengthLongerThanStream() { diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.0.dump index f76b5f9d18f..73189b48147 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.0.dump @@ -11,7 +11,7 @@ track 0: sample count = 117 track duration = 2807979 format 0: - averageBitrate = 32000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.1.dump index f76b5f9d18f..73189b48147 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.1.dump @@ -11,7 +11,7 @@ track 0: sample count = 117 track duration = 2807979 format 0: - averageBitrate = 32000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.2.dump index 05cb272f2fd..ff21a50a27c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.2.dump @@ -11,7 +11,7 @@ track 0: sample count = 88 track duration = 2807979 format 0: - averageBitrate = 32000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.3.dump index 05cb272f2fd..ff21a50a27c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.3.dump @@ -11,7 +11,7 @@ track 0: sample count = 88 track duration = 2807979 format 0: - averageBitrate = 32000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.unknown_length.dump index f76b5f9d18f..73189b48147 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.unknown_length.dump @@ -11,7 +11,7 @@ track 0: sample count = 117 track duration = 2807979 format 0: - averageBitrate = 32000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.0.dump index 9bbdfa04026..22dd2fe8b76 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.0.dump @@ -11,7 +11,7 @@ track 0: sample count = 117 track duration = 2807979 format 0: - averageBitrate = 32000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.1.dump index 0c709cfaeae..93a0ab2836b 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.1.dump @@ -11,7 +11,7 @@ track 0: sample count = 88 track duration = 2807979 format 0: - averageBitrate = 32000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.2.dump index 4fcaba8671b..5a224a448f6 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.2.dump @@ -11,7 +11,7 @@ track 0: sample count = 59 track duration = 2807979 format 0: - averageBitrate = 32000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.3.dump index 272241c5906..04293198ca5 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.3.dump @@ -11,7 +11,7 @@ track 0: sample count = 30 track duration = 2807979 format 0: - averageBitrate = 32000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.unknown_length.dump index 9bbdfa04026..22dd2fe8b76 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header.mp3.unknown_length.dump @@ -11,7 +11,7 @@ track 0: sample count = 117 track duration = 2807979 format 0: - averageBitrate = 32000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header-no-toc.mp3.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header-no-toc.mp3.0.dump index 5cfd3266218..82a1edbcf62 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header-no-toc.mp3.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header-no-toc.mp3.0.dump @@ -8,7 +8,7 @@ track 0: sample count = 117 track duration = 2807979 format 0: - averageBitrate = 64000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header-no-toc.mp3.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header-no-toc.mp3.unknown_length.dump index 5cfd3266218..82a1edbcf62 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header-no-toc.mp3.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header-no-toc.mp3.unknown_length.dump @@ -8,7 +8,7 @@ track 0: sample count = 117 track duration = 2807979 format 0: - averageBitrate = 64000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.0.dump index 9dde5408e01..b5e7e19f405 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.0.dump @@ -11,7 +11,7 @@ track 0: sample count = 117 track duration = 2807979 format 0: - averageBitrate = 64000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.1.dump index b47319b0f7c..0dc3abaf2af 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.1.dump @@ -11,7 +11,7 @@ track 0: sample count = 77 track duration = 2807979 format 0: - averageBitrate = 64000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.2.dump index 7eabbad4cb7..5cb352f5b1f 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.2.dump @@ -11,7 +11,7 @@ track 0: sample count = 38 track duration = 2807979 format 0: - averageBitrate = 64000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.3.dump index 47ba03e7576..345a32a31d3 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.3.dump @@ -11,7 +11,7 @@ track 0: sample count = 0 track duration = 2807979 format 0: - averageBitrate = 64000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.unknown_length.dump index 9dde5408e01..b5e7e19f405 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.unknown_length.dump @@ -11,7 +11,7 @@ track 0: sample count = 117 track duration = 2807979 format 0: - averageBitrate = 64000 + averageBitrate = 108719 containerMimeType = audio/mpeg sampleMimeType = audio/mpeg maxInputSize = 4096 From 48836cb9febaf0327c6863f12ef3ecb836112dc8 Mon Sep 17 00:00:00 2001 From: Ian Baker Date: Wed, 27 May 2026 12:14:25 +0100 Subject: [PATCH 2/5] Move bitrate computation to shared `Mp3Util` --- .../media3/extractor/mp3/IndexSeeker.java | 17 ++------- .../media3/extractor/mp3/Mp3Util.java | 37 +++++++++++++++++++ .../media3/extractor/mp3/VbriSeeker.java | 27 ++------------ .../media3/extractor/mp3/XingFrame.java | 24 ------------ .../media3/extractor/mp3/XingSeeker.java | 5 +-- .../media3/extractor/mp3/VbriSeekerTest.java | 5 +-- 6 files changed, 47 insertions(+), 68 deletions(-) create mode 100644 libraries/extractor/src/main/java/androidx/media3/extractor/mp3/Mp3Util.java diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/IndexSeeker.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/IndexSeeker.java index c6c28b3b0e9..88d0955875e 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/IndexSeeker.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/IndexSeeker.java @@ -15,11 +15,11 @@ */ package androidx.media3.extractor.mp3; +import static androidx.media3.extractor.mp3.Mp3Util.computeAverageBitrate; + import androidx.annotation.VisibleForTesting; import androidx.media3.common.C; -import androidx.media3.common.util.Util; import androidx.media3.extractor.IndexSeekMap; -import java.math.RoundingMode; /** MP3 seeker that builds a time-to-byte mapping as the stream is read. */ /* package */ final class IndexSeeker implements Seeker { @@ -40,18 +40,7 @@ public IndexSeeker(long durationUs, long dataStartPosition, long dataEndPosition durationUs); this.dataStartPosition = dataStartPosition; this.dataEndPosition = dataEndPosition; - if (durationUs != C.TIME_UNSET) { - long bitrate = - Util.scaleLargeValue( - dataEndPosition - dataStartPosition, - C.BITS_PER_BYTE * C.MICROS_PER_SECOND, - durationUs, - RoundingMode.HALF_UP); - this.averageBitrate = - bitrate > 0 && bitrate <= Integer.MAX_VALUE ? (int) bitrate : C.RATE_UNSET_INT; - } else { - this.averageBitrate = C.RATE_UNSET_INT; - } + this.averageBitrate = computeAverageBitrate(dataEndPosition - dataStartPosition, durationUs); } @Override diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/Mp3Util.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/Mp3Util.java new file mode 100644 index 00000000000..cc5e06d3caf --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/Mp3Util.java @@ -0,0 +1,37 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.extractor.mp3; + +import androidx.media3.common.C; +import androidx.media3.common.util.Util; +import java.math.RoundingMode; + +/* package */ class Mp3Util { + + /* package */ static int computeAverageBitrate(long dataSize, long durationUs) { + if (dataSize <= 0 || durationUs <= 0) { + return C.RATE_UNSET_INT; + } + long averageBitrate = + Util.scaleLargeValue( + dataSize, C.BITS_PER_BYTE * C.MICROS_PER_SECOND, durationUs, RoundingMode.HALF_UP); + return averageBitrate > 0 && averageBitrate <= Integer.MAX_VALUE + ? (int) averageBitrate + : C.RATE_UNSET_INT; + } + + private Mp3Util() {} +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/VbriSeeker.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/VbriSeeker.java index 671bb0d8497..437b33a4452 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/VbriSeeker.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/VbriSeeker.java @@ -15,6 +15,7 @@ */ package androidx.media3.extractor.mp3; +import static androidx.media3.extractor.mp3.Mp3Util.computeAverageBitrate; import static java.lang.Math.max; import androidx.annotation.Nullable; @@ -24,7 +25,6 @@ import androidx.media3.common.util.Util; import androidx.media3.extractor.MpegAudioUtil; import androidx.media3.extractor.SeekPoint; -import java.math.RoundingMode; /** MP3 seeker that uses metadata from a VBRI header. */ /* package */ final class VbriSeeker implements Seeker { @@ -107,13 +107,7 @@ public static VbriSeeker create( endOfMp3Data = max(endOfMp3Data, position); } - return new VbriSeeker( - timesUs, - positions, - durationUs, - startOfMp3Data, - endOfMp3Data, - computeAverageBitrate(endOfMp3Data - startOfMp3Data, durationUs)); + return new VbriSeeker(timesUs, positions, durationUs, startOfMp3Data, endOfMp3Data); } private final long[] timesUs; @@ -128,14 +122,13 @@ private VbriSeeker( long[] positions, long durationUs, long dataStartPosition, - long dataEndPosition, - int averageBitrate) { + long dataEndPosition) { this.timesUs = timesUs; this.positions = positions; this.durationUs = durationUs; this.dataStartPosition = dataStartPosition; this.dataEndPosition = dataEndPosition; - this.averageBitrate = averageBitrate; + this.averageBitrate = computeAverageBitrate(dataEndPosition - dataStartPosition, durationUs); } @Override @@ -179,16 +172,4 @@ public long getDataEndPosition() { public int getAverageBitrate() { return averageBitrate; } - - private static int computeAverageBitrate(long dataSize, long durationUs) { - if (dataSize <= 0 || durationUs <= 0) { - return C.RATE_UNSET_INT; - } - long averageBitrate = - Util.scaleLargeValue( - dataSize, C.BITS_PER_BYTE * C.MICROS_PER_SECOND, durationUs, RoundingMode.HALF_UP); - return averageBitrate > 0 && averageBitrate <= Integer.MAX_VALUE - ? (int) averageBitrate - : C.RATE_UNSET_INT; - } } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingFrame.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingFrame.java index 9326fdcb210..3839596622b 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingFrame.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingFrame.java @@ -156,30 +156,6 @@ public long computeDurationUs() { (frameCount * header.samplesPerFrame) - 1, header.sampleRate); } - /** - * Computes the average bitrate, in bits per second, represented by this frame and {@code - * dataSize}. Returns {@link C#RATE_UNSET_INT} if it can't be computed. - * - * @param dataSize The encoded stream size, including the Xing or Info frame. - */ - public int computeAverageBitrate(long dataSize) { - if (frameCount == C.LENGTH_UNSET - || frameCount == 0 - || dataSize == C.LENGTH_UNSET - || dataSize <= header.frameSize) { - return C.RATE_UNSET_INT; - } - long averageBitrate = - Util.scaleLargeValue( - dataSize - header.frameSize, - C.BITS_PER_BYTE * C.MICROS_PER_SECOND, - computeDurationUs(), - RoundingMode.HALF_UP); - return averageBitrate > 0 && averageBitrate <= Integer.MAX_VALUE - ? (int) averageBitrate - : C.RATE_UNSET_INT; - } - /** Provide the metadata derived from this Xing frame, such as ReplayGain data. */ @Nullable public Metadata getMetadata() { diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingSeeker.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingSeeker.java index b39b24e132f..90721b2ae9a 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingSeeker.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingSeeker.java @@ -15,6 +15,7 @@ */ package androidx.media3.extractor.mp3; +import static androidx.media3.extractor.mp3.Mp3Util.computeAverageBitrate; import static com.google.common.base.Preconditions.checkNotNull; import androidx.annotation.Nullable; @@ -65,7 +66,6 @@ public static XingSeeker create(XingFrame xingFrame, long position, long streamL position, xingFrame.header.frameSize, durationUs, - xingFrame.computeAverageBitrate(dataSize), dataSize, xingFrame.tableOfContents); } @@ -90,13 +90,12 @@ private XingSeeker( long dataStartPosition, int xingFrameSize, long durationUs, - int averageBitrate, long dataSize, @Nullable long[] tableOfContents) { this.dataStartPosition = dataStartPosition; this.xingFrameSize = xingFrameSize; this.durationUs = durationUs; - this.averageBitrate = averageBitrate; + this.averageBitrate = computeAverageBitrate(dataSize - xingFrameSize, durationUs); this.dataSize = dataSize; this.tableOfContents = tableOfContents; dataEndPosition = dataSize == C.LENGTH_UNSET ? C.INDEX_UNSET : dataStartPosition + dataSize; diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/VbriSeekerTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/VbriSeekerTest.java index b1bdb9fccf4..d4dce5f79e0 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/VbriSeekerTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/VbriSeekerTest.java @@ -54,10 +54,7 @@ public void getAverageBitrate_returnsAverageFromDataSizeAndDuration() { int expectedAverageBitrate = (int) Util.scaleLargeValue( - dataSize, - C.BITS_PER_BYTE * C.MICROS_PER_SECOND, - durationUs, - RoundingMode.HALF_UP); + dataSize, C.BITS_PER_BYTE * C.MICROS_PER_SECOND, durationUs, RoundingMode.HALF_UP); assertThat(seeker.getAverageBitrate()).isEqualTo(expectedAverageBitrate); assertThat(seeker.getAverageBitrate()).isNotEqualTo(header.bitrate); From 99fb323791449ca83130dbfe5f79a1056b476bb7 Mon Sep 17 00:00:00 2001 From: Ian Baker Date: Thu, 28 May 2026 17:37:09 +0100 Subject: [PATCH 3/5] Add release note --- RELEASENOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ab3a49c6fe2..ec7f0423cea 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -102,6 +102,7 @@ * MPEG-TS: Ensure the last frame is rendered for streams where the last PES packet has a known length ([#3206](https://github.com/androidx/media/pull/3206)). + * MP3: Fix bitrate reporting for files with Xing and VBRI header. * Inspector: * Audio: * Add a 100ms grace period in ExoPlayer's audio renderers when From c231d0211e9de9b2e250343566ecf8c83bc87e50 Mon Sep 17 00:00:00 2001 From: Ian Baker Date: Thu, 28 May 2026 17:41:30 +0100 Subject: [PATCH 4/5] Remove unused import --- .../src/main/java/androidx/media3/extractor/mp3/XingFrame.java | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingFrame.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingFrame.java index 3839596622b..b62e0f074e2 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingFrame.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingFrame.java @@ -21,7 +21,6 @@ import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.Util; import androidx.media3.extractor.MpegAudioUtil; -import java.math.RoundingMode; /** Representation of a LAME Xing or Info frame. */ /* package */ final class XingFrame { From a4f7d561ac7514f9cdcaf6dcdd4df28054a5eb26 Mon Sep 17 00:00:00 2001 From: Ian Baker Date: Thu, 28 May 2026 17:42:59 +0100 Subject: [PATCH 5/5] Fix param comment --- .../test/java/androidx/media3/extractor/mp3/VbriSeekerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/VbriSeekerTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/VbriSeekerTest.java index d4dce5f79e0..e71f2d0f0cc 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/VbriSeekerTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/VbriSeekerTest.java @@ -47,7 +47,7 @@ public void getAverageBitrate_returnsAverageFromDataSizeAndDuration() { /* inputLength= */ C.LENGTH_UNSET, VBRI_FRAME_POSITION, header, - createVbriFrame(dataSize, frameCount, /* segmentSizes= */ 400, 600))); + createVbriFrame(dataSize, frameCount, /* segmentSizes...= */ 400, 600))); long durationUs = Util.sampleCountToDurationUs( ((long) frameCount * header.samplesPerFrame) - 1, header.sampleRate);