Skip to content

Commit 6a33660

Browse files
Merge pull request #3231 from Tolriq:mp3-average-bitrate
PiperOrigin-RevId: 923339186
2 parents 4dc0fb3 + a4f7d56 commit 6a33660

24 files changed

Lines changed: 162 additions & 48 deletions

RELEASENOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
* MPEG-TS: Ensure the last frame is rendered for streams where the last
107107
PES packet has a known length
108108
([#3206](https://github.com/androidx/media/pull/3206)).
109+
* MP3: Fix bitrate reporting for files with Xing and VBRI header.
109110
* Inspector:
110111
* Audio:
111112
* Add a 100ms grace period in ExoPlayer's audio renderers when

libraries/extractor/src/main/java/androidx/media3/extractor/mp3/IndexSeeker.java

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
*/
1616
package androidx.media3.extractor.mp3;
1717

18+
import static androidx.media3.extractor.mp3.Mp3Util.computeAverageBitrate;
19+
1820
import androidx.annotation.VisibleForTesting;
1921
import androidx.media3.common.C;
20-
import androidx.media3.common.util.Util;
2122
import androidx.media3.extractor.IndexSeekMap;
22-
import java.math.RoundingMode;
2323

2424
/** MP3 seeker that builds a time-to-byte mapping as the stream is read. */
2525
/* package */ final class IndexSeeker implements Seeker {
@@ -40,18 +40,7 @@ public IndexSeeker(long durationUs, long dataStartPosition, long dataEndPosition
4040
durationUs);
4141
this.dataStartPosition = dataStartPosition;
4242
this.dataEndPosition = dataEndPosition;
43-
if (durationUs != C.TIME_UNSET) {
44-
long bitrate =
45-
Util.scaleLargeValue(
46-
dataEndPosition - dataStartPosition,
47-
C.BITS_PER_BYTE * C.MICROS_PER_SECOND,
48-
durationUs,
49-
RoundingMode.HALF_UP);
50-
this.averageBitrate =
51-
bitrate > 0 && bitrate <= Integer.MAX_VALUE ? (int) bitrate : C.RATE_UNSET_INT;
52-
} else {
53-
this.averageBitrate = C.RATE_UNSET_INT;
54-
}
43+
this.averageBitrate = computeAverageBitrate(dataEndPosition - dataStartPosition, durationUs);
5544
}
5645

5746
@Override
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package androidx.media3.extractor.mp3;
17+
18+
import androidx.media3.common.C;
19+
import androidx.media3.common.util.Util;
20+
import java.math.RoundingMode;
21+
22+
/* package */ class Mp3Util {
23+
24+
/* package */ static int computeAverageBitrate(long dataSize, long durationUs) {
25+
if (dataSize <= 0 || durationUs <= 0) {
26+
return C.RATE_UNSET_INT;
27+
}
28+
long averageBitrate =
29+
Util.scaleLargeValue(
30+
dataSize, C.BITS_PER_BYTE * C.MICROS_PER_SECOND, durationUs, RoundingMode.HALF_UP);
31+
return averageBitrate > 0 && averageBitrate <= Integer.MAX_VALUE
32+
? (int) averageBitrate
33+
: C.RATE_UNSET_INT;
34+
}
35+
36+
private Mp3Util() {}
37+
}

libraries/extractor/src/main/java/androidx/media3/extractor/mp3/VbriSeeker.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package androidx.media3.extractor.mp3;
1717

18+
import static androidx.media3.extractor.mp3.Mp3Util.computeAverageBitrate;
1819
import static java.lang.Math.max;
1920

2021
import androidx.annotation.Nullable;
@@ -106,30 +107,28 @@ public static VbriSeeker create(
106107
endOfMp3Data = max(endOfMp3Data, position);
107108
}
108109

109-
return new VbriSeeker(
110-
timesUs, positions, durationUs, startOfMp3Data, endOfMp3Data, mpegAudioHeader.bitrate);
110+
return new VbriSeeker(timesUs, positions, durationUs, startOfMp3Data, endOfMp3Data);
111111
}
112112

113113
private final long[] timesUs;
114114
private final long[] positions;
115115
private final long durationUs;
116116
private final long dataStartPosition;
117117
private final long dataEndPosition;
118-
private final int bitrate;
118+
private final int averageBitrate;
119119

120120
private VbriSeeker(
121121
long[] timesUs,
122122
long[] positions,
123123
long durationUs,
124124
long dataStartPosition,
125-
long dataEndPosition,
126-
int bitrate) {
125+
long dataEndPosition) {
127126
this.timesUs = timesUs;
128127
this.positions = positions;
129128
this.durationUs = durationUs;
130129
this.dataStartPosition = dataStartPosition;
131130
this.dataEndPosition = dataEndPosition;
132-
this.bitrate = bitrate;
131+
this.averageBitrate = computeAverageBitrate(dataEndPosition - dataStartPosition, durationUs);
133132
}
134133

135134
@Override
@@ -171,6 +170,6 @@ public long getDataEndPosition() {
171170

172171
@Override
173172
public int getAverageBitrate() {
174-
return bitrate;
173+
return averageBitrate;
175174
}
176175
}

libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingSeeker.java

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package androidx.media3.extractor.mp3;
1717

18+
import static androidx.media3.extractor.mp3.Mp3Util.computeAverageBitrate;
1819
import static com.google.common.base.Preconditions.checkNotNull;
1920

2021
import androidx.annotation.Nullable;
@@ -62,18 +63,13 @@ public static XingSeeker create(XingFrame xingFrame, long position, long streamL
6263
dataSize = xingFrame.dataSize;
6364
}
6465
return new XingSeeker(
65-
position,
66-
xingFrame.header.frameSize,
67-
durationUs,
68-
xingFrame.header.bitrate,
69-
dataSize,
70-
xingFrame.tableOfContents);
66+
position, xingFrame.header.frameSize, durationUs, dataSize, xingFrame.tableOfContents);
7167
}
7268

7369
private final long dataStartPosition;
7470
private final int xingFrameSize;
7571
private final long durationUs;
76-
private final int bitrate;
72+
private final int averageBitrate;
7773

7874
/** Data size, including the XING frame. */
7975
private final long dataSize;
@@ -90,13 +86,12 @@ private XingSeeker(
9086
long dataStartPosition,
9187
int xingFrameSize,
9288
long durationUs,
93-
int bitrate,
9489
long dataSize,
9590
@Nullable long[] tableOfContents) {
9691
this.dataStartPosition = dataStartPosition;
9792
this.xingFrameSize = xingFrameSize;
9893
this.durationUs = durationUs;
99-
this.bitrate = bitrate;
94+
this.averageBitrate = computeAverageBitrate(dataSize - xingFrameSize, durationUs);
10095
this.dataSize = dataSize;
10196
this.tableOfContents = tableOfContents;
10297
dataEndPosition = dataSize == C.LENGTH_UNSET ? C.INDEX_UNSET : dataStartPosition + dataSize;
@@ -173,7 +168,7 @@ public long getDataEndPosition() {
173168

174169
@Override
175170
public int getAverageBitrate() {
176-
return bitrate;
171+
return averageBitrate;
177172
}
178173

179174
/**
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package androidx.media3.extractor.mp3;
17+
18+
import static com.google.common.base.Preconditions.checkNotNull;
19+
import static com.google.common.truth.Truth.assertThat;
20+
21+
import androidx.media3.common.C;
22+
import androidx.media3.common.util.ParsableByteArray;
23+
import androidx.media3.common.util.Util;
24+
import androidx.media3.extractor.MpegAudioUtil;
25+
import androidx.test.ext.junit.runners.AndroidJUnit4;
26+
import java.math.RoundingMode;
27+
import java.nio.ByteBuffer;
28+
import org.junit.Test;
29+
import org.junit.runner.RunWith;
30+
31+
/** Tests for {@link VbriSeeker}. */
32+
@RunWith(AndroidJUnit4.class)
33+
public final class VbriSeekerTest {
34+
35+
private static final int VBRI_FRAME_HEADER_DATA = 0xFFFB3000;
36+
private static final int VBRI_FRAME_POSITION = 157;
37+
38+
@Test
39+
public void getAverageBitrate_returnsAverageFromDataSizeAndDuration() {
40+
MpegAudioUtil.Header header = new MpegAudioUtil.Header();
41+
header.setForHeaderData(VBRI_FRAME_HEADER_DATA);
42+
int dataSize = 1_000;
43+
int frameCount = 40;
44+
VbriSeeker seeker =
45+
checkNotNull(
46+
VbriSeeker.create(
47+
/* inputLength= */ C.LENGTH_UNSET,
48+
VBRI_FRAME_POSITION,
49+
header,
50+
createVbriFrame(dataSize, frameCount, /* segmentSizes...= */ 400, 600)));
51+
long durationUs =
52+
Util.sampleCountToDurationUs(
53+
((long) frameCount * header.samplesPerFrame) - 1, header.sampleRate);
54+
int expectedAverageBitrate =
55+
(int)
56+
Util.scaleLargeValue(
57+
dataSize, C.BITS_PER_BYTE * C.MICROS_PER_SECOND, durationUs, RoundingMode.HALF_UP);
58+
59+
assertThat(seeker.getAverageBitrate()).isEqualTo(expectedAverageBitrate);
60+
assertThat(seeker.getAverageBitrate()).isNotEqualTo(header.bitrate);
61+
}
62+
63+
private static ParsableByteArray createVbriFrame(
64+
int dataSize, int frameCount, int... segmentSizes) {
65+
ByteBuffer payload = ByteBuffer.allocate(6 + 4 + 4 + 2 + 2 + 2 + 2 + 2 * segmentSizes.length);
66+
payload.position(6);
67+
payload.putInt(dataSize);
68+
payload.putInt(frameCount);
69+
payload.putShort((short) segmentSizes.length);
70+
payload.putShort((short) 1); // scale
71+
payload.putShort((short) 2); // entry size
72+
payload.putShort((short) 0);
73+
for (int segmentSize : segmentSizes) {
74+
payload.putShort((short) segmentSize);
75+
}
76+
return new ParsableByteArray(payload.array());
77+
}
78+
}

libraries/extractor/src/test/java/androidx/media3/extractor/mp3/XingSeekerTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import androidx.media3.extractor.SeekMap.SeekPoints;
2525
import androidx.media3.extractor.SeekPoint;
2626
import androidx.test.ext.junit.runners.AndroidJUnit4;
27+
import java.math.RoundingMode;
2728
import org.junit.Before;
2829
import org.junit.Test;
2930
import org.junit.runner.RunWith;
@@ -86,6 +87,20 @@ public void getTimeUsAtEndOfStream() {
8687
assertThat(seeker.getTimeUs(XING_AUDIO_END_POSITION)).isEqualTo(XING_STREAM_DURATION_US);
8788
}
8889

90+
@Test
91+
public void getAverageBitrate_returnsAverageFromDataSizeAndDuration() {
92+
int expectedAverageBitrate =
93+
(int)
94+
Util.scaleLargeValue(
95+
XING_FRAME.dataSize - XING_FRAME.header.frameSize,
96+
C.BITS_PER_BYTE * C.MICROS_PER_SECOND,
97+
XING_STREAM_DURATION_US,
98+
RoundingMode.HALF_UP);
99+
100+
assertThat(seeker.getAverageBitrate()).isEqualTo(expectedAverageBitrate);
101+
assertThat(seeker.getAverageBitrate()).isNotEqualTo(XING_FRAME.header.bitrate);
102+
}
103+
89104
// https://github.com/androidx/media/issues/3117#issuecomment-4046538506
90105
@Test
91106
public void getTimeUsAtEndOfStream_xingLengthLongerThanStream() {

libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.0.dump

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ track 0:
1111
sample count = 117
1212
track duration = 2807979
1313
format 0:
14-
averageBitrate = 32000
14+
averageBitrate = 108719
1515
containerMimeType = audio/mpeg
1616
sampleMimeType = audio/mpeg
1717
maxInputSize = 4096

libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.1.dump

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ track 0:
1111
sample count = 117
1212
track duration = 2807979
1313
format 0:
14-
averageBitrate = 32000
14+
averageBitrate = 108719
1515
containerMimeType = audio/mpeg
1616
sampleMimeType = audio/mpeg
1717
maxInputSize = 4096

libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-vbri-header-truncated-toc.mp3.2.dump

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ track 0:
1111
sample count = 88
1212
track duration = 2807979
1313
format 0:
14-
averageBitrate = 32000
14+
averageBitrate = 108719
1515
containerMimeType = audio/mpeg
1616
sampleMimeType = audio/mpeg
1717
maxInputSize = 4096

0 commit comments

Comments
 (0)