1515 */
1616package androidx .media3 .extractor .mp3 ;
1717
18+ import static androidx .media3 .extractor .mp3 .Mp3Util .computeAverageBitrate ;
1819import static com .google .common .base .Preconditions .checkNotNull ;
1920import static java .lang .annotation .ElementType .TYPE_USE ;
2021import static java .lang .annotation .RetentionPolicy .SOURCE ;
@@ -264,7 +265,7 @@ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOExce
264265 int readResult = readInternal (input );
265266 if (readResult == RESULT_END_OF_INPUT && seeker instanceof IndexSeeker ) {
266267 // Duration is exact when index seeker is used.
267- long durationUs = computeTimeUs (samplesRead );
268+ long durationUs = computeFinalIndexSeekerDurationUs (samplesRead );
268269 if (seeker .getDurationUs () != durationUs ) {
269270 ((IndexSeeker ) seeker ).setDurationUs (durationUs );
270271 extractorOutput .seekMap (seeker );
@@ -389,6 +390,28 @@ private long computeTimeUs(long samplesRead) {
389390 return basisTimeUs + samplesRead * C .MICROS_PER_SECOND / synchronizedHeader .sampleRate ;
390391 }
391392
393+ /**
394+ * Returns the final duration to expose for an {@link IndexSeeker}.
395+ *
396+ * <p>Index seeking finalizes duration from the encoded samples read at EOF. When gapless metadata
397+ * is present, this trims the encoder delay and padding so EOF finalization does not replace an
398+ * initially gapless Xing/Info duration with the longer encoded duration.
399+ */
400+ private long computeFinalIndexSeekerDurationUs (long samplesRead ) {
401+ long durationUs = computeTimeUs (samplesRead );
402+ if (!gaplessInfoHolder .hasGaplessInfo ()) {
403+ return durationUs ;
404+ }
405+ long finalGaplessSampleIndex =
406+ Util .durationUsToSampleCount (durationUs , synchronizedHeader .sampleRate )
407+ - gaplessInfoHolder .encoderDelay
408+ - gaplessInfoHolder .encoderPadding
409+ - 1 ;
410+ return finalGaplessSampleIndex >= 0
411+ ? Util .sampleCountToDurationUs (finalGaplessSampleIndex , synchronizedHeader .sampleRate )
412+ : C .TIME_UNSET ;
413+ }
414+
392415 private boolean synchronize (ExtractorInput input , boolean sniffing ) throws IOException {
393416 int validFrameCount = 0 ;
394417 int candidateSynchronizedHeaderData = 0 ;
@@ -516,15 +539,13 @@ private Seeker computeSeeker(ExtractorInput input) throws IOException {
516539 new IndexSeeker (
517540 resultSeeker .getDurationUs (),
518541 /* dataStartPosition= */ input .getPosition (),
519- resultSeeker .getDataEndPosition ());
542+ resultSeeker .getDataEndPosition (),
543+ resultSeeker .getAverageBitrate ());
520544 }
521545
522- if (shouldFallbackToConstantBitrateSeeking (resultSeeker )
523- && resultSeeker .getDurationUs () != C .TIME_UNSET
524- && (resultSeeker .getDataEndPosition () != C .INDEX_UNSET
525- || input .getLength () != C .LENGTH_UNSET )) {
526- // resultSeeker does not allow seeking, but does provide a duration and constant bitrate
527- // seeking has been requested, so we can do 'enhanced' CBR seeking using this duration info.
546+ if (shouldFallbackToConstantBitrateSeeking (resultSeeker )) {
547+ // If resultSeeker does not allow seeking but provides a duration and known end position, use
548+ // this info to do 'enhanced' CBR seeking.
528549 long dataStart =
529550 resultSeeker .getDataStartPosition () != C .INDEX_UNSET
530551 ? resultSeeker .getDataStartPosition ()
@@ -533,24 +554,26 @@ private Seeker computeSeeker(ExtractorInput input) throws IOException {
533554 resultSeeker .getDataEndPosition () != C .INDEX_UNSET
534555 ? resultSeeker .getDataEndPosition ()
535556 : input .getLength ();
536- long audioLength = inputLength - dataStart ;
537- int bitrate =
538- Ints .saturatedCast (
539- Util .scaleLargeValue (
540- audioLength ,
541- Byte .SIZE * C .MICROS_PER_SECOND ,
542- resultSeeker .getDurationUs (),
543- RoundingMode .HALF_UP ));
544- // inputLength will never be LENGTH_UNSET because of the outer if-condition, so we can pass
545- // (vacuously) false here for allowSeeksIfLengthUnknown.
546- resultSeeker =
547- new ConstantBitrateSeeker (
548- inputLength ,
549- dataStart ,
550- bitrate ,
551- C .LENGTH_UNSET ,
552- /* allowSeeksIfLengthUnknown= */ false );
553- } else if (shouldFallbackToConstantBitrateSeeking (resultSeeker )) {
557+ long durationUs = resultSeeker .getDurationUs ();
558+ if (durationUs != C .TIME_UNSET && inputLength != C .LENGTH_UNSET ) {
559+ int averageBitrate = computeAverageBitrate (inputLength - dataStart , durationUs );
560+ if (averageBitrate != C .RATE_UNSET_INT ) {
561+ // Only use enhanced CBR seeking when its bitrate can be derived safely. Otherwise, the
562+ // regular CBR fallback below will use the next frame header bitrate.
563+ // inputLength is known, so we can pass (vacuously) false for allowSeeksIfLengthUnknown.
564+ resultSeeker =
565+ new ConstantBitrateSeeker (
566+ inputLength ,
567+ dataStart ,
568+ averageBitrate ,
569+ C .LENGTH_UNSET ,
570+ /* allowSeeksIfLengthUnknown= */ false ,
571+ durationUs );
572+ }
573+ }
574+ }
575+
576+ if (shouldFallbackToConstantBitrateSeeking (resultSeeker )) {
554577 // Either we found no seek or VBR info, so we must assume the file is CBR (even without the
555578 // flag(s) being set), or an 'enable CBR seeking flag' is set and we found some seek info, but
556579 // not enough to do 'enhanced' CBR seeking with. In either case, we fall back to CBR seeking
@@ -670,15 +693,13 @@ private Seeker getConstantBitrateSeeker(
670693
671694 // Derive the bitrate and frame size by averaging over the length of playable audio, to allow
672695 // for 'mostly' CBR streams that might have a small number of frames with a different bitrate.
673- // We can assume infoFrame.frameCount is set, because otherwise computeDurationUs() would
674- // have returned C.TIME_UNSET above. See also https://github.com/androidx/media/issues/1376.
675- int averageBitrate =
676- Ints .checkedCast (
677- Util .scaleLargeValue (
678- audioLength ,
679- C .BITS_PER_BYTE * C .MICROS_PER_SECOND ,
680- durationUs ,
681- RoundingMode .HALF_UP ));
696+ // See also https://github.com/androidx/media/issues/1376.
697+ int averageBitrate = computeAverageBitrate (audioLength , durationUs );
698+ if (averageBitrate == C .RATE_UNSET_INT ) {
699+ // Invalid Info sizes or durations should fall back to the next frame header bitrate rather
700+ // than constructing a ConstantBitrateSeeker with an unset bitrate.
701+ return null ;
702+ }
682703 int frameSize =
683704 Ints .checkedCast (LongMath .divide (audioLength , infoFrame .frameCount , RoundingMode .HALF_UP ));
684705 // Set the seeker frame size to the average frame size (even though some constant bitrate
@@ -689,7 +710,8 @@ private Seeker getConstantBitrateSeeker(
689710 /* firstFramePosition= */ infoFramePosition + infoFrame .header .frameSize ,
690711 averageBitrate ,
691712 frameSize ,
692- /* allowSeeksIfLengthUnknown= */ false );
713+ /* allowSeeksIfLengthUnknown= */ false ,
714+ durationUs );
693715 }
694716
695717 /**
0 commit comments