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 ;
@@ -519,12 +542,9 @@ private Seeker computeSeeker(ExtractorInput input) throws IOException {
519542 resultSeeker .getDataEndPosition ());
520543 }
521544
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.
545+ if (shouldFallbackToConstantBitrateSeeking (resultSeeker )) {
546+ // If resultSeeker does not allow seeking but provides a duration and known end position, use
547+ // this info to do 'enhanced' CBR seeking.
528548 long dataStart =
529549 resultSeeker .getDataStartPosition () != C .INDEX_UNSET
530550 ? resultSeeker .getDataStartPosition ()
@@ -533,24 +553,26 @@ private Seeker computeSeeker(ExtractorInput input) throws IOException {
533553 resultSeeker .getDataEndPosition () != C .INDEX_UNSET
534554 ? resultSeeker .getDataEndPosition ()
535555 : 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 )) {
556+ long durationUs = resultSeeker .getDurationUs ();
557+ if (durationUs != C .TIME_UNSET && inputLength != C .LENGTH_UNSET ) {
558+ int averageBitrate = computeAverageBitrate (inputLength - dataStart , durationUs );
559+ if (averageBitrate != C .RATE_UNSET_INT ) {
560+ // Only use enhanced CBR seeking when its bitrate can be derived safely. Otherwise, the
561+ // regular CBR fallback below will use the next frame header bitrate.
562+ // inputLength is known, so we can pass (vacuously) false for allowSeeksIfLengthUnknown.
563+ resultSeeker =
564+ new ConstantBitrateSeeker (
565+ inputLength ,
566+ dataStart ,
567+ averageBitrate ,
568+ C .LENGTH_UNSET ,
569+ /* allowSeeksIfLengthUnknown= */ false ,
570+ durationUs );
571+ }
572+ }
573+ }
574+
575+ if (shouldFallbackToConstantBitrateSeeking (resultSeeker )) {
554576 // Either we found no seek or VBR info, so we must assume the file is CBR (even without the
555577 // flag(s) being set), or an 'enable CBR seeking flag' is set and we found some seek info, but
556578 // not enough to do 'enhanced' CBR seeking with. In either case, we fall back to CBR seeking
@@ -670,15 +692,13 @@ private Seeker getConstantBitrateSeeker(
670692
671693 // Derive the bitrate and frame size by averaging over the length of playable audio, to allow
672694 // 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 ));
695+ // See also https://github.com/androidx/media/issues/1376.
696+ int averageBitrate = computeAverageBitrate (audioLength , durationUs );
697+ if (averageBitrate == C .RATE_UNSET_INT ) {
698+ // Invalid Info sizes or durations should fall back to the next frame header bitrate rather
699+ // than constructing a ConstantBitrateSeeker with an unset bitrate.
700+ return null ;
701+ }
682702 int frameSize =
683703 Ints .checkedCast (LongMath .divide (audioLength , infoFrame .frameCount , RoundingMode .HALF_UP ));
684704 // Set the seeker frame size to the average frame size (even though some constant bitrate
@@ -689,7 +709,8 @@ private Seeker getConstantBitrateSeeker(
689709 /* firstFramePosition= */ infoFramePosition + infoFrame .header .frameSize ,
690710 averageBitrate ,
691711 frameSize ,
692- /* allowSeeksIfLengthUnknown= */ false );
712+ /* allowSeeksIfLengthUnknown= */ false ,
713+ durationUs );
693714 }
694715
695716 /**
0 commit comments