diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelection.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelection.java index e3286375c5..3e4dd4d41d 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelection.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelection.java @@ -461,8 +461,10 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { // Revert back to the previous selection if conditions are not suitable for switching. Format currentFormat = getFormat(previousSelectedIndex); Format selectedFormat = getFormat(newSelectedIndex); + long minDurationForQualityIncreaseUs = + minDurationForQualityIncreaseUs(availableDurationUs, chunkDurationUs); if (selectedFormat.bitrate > currentFormat.bitrate - && bufferedDurationUs < minDurationForQualityIncreaseUs(availableDurationUs)) { + && bufferedDurationUs < minDurationForQualityIncreaseUs) { // The selected track is a higher quality, but we have insufficient buffer to safely switch // up. Defer switching up for now. newSelectedIndex = previousSelectedIndex; @@ -602,13 +604,22 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { return lowestBitrateAllowedIndex; } - private long minDurationForQualityIncreaseUs(long availableDurationUs) { + private long minDurationForQualityIncreaseUs(long availableDurationUs, long chunkDurationUs) { boolean isAvailableDurationTooShort = availableDurationUs != C.TIME_UNSET && availableDurationUs <= minDurationForQualityIncreaseUs; - return isAvailableDurationTooShort - ? (long) (availableDurationUs * bufferedFractionToLiveEdgeForQualityIncrease) - : minDurationForQualityIncreaseUs; + if (!isAvailableDurationTooShort) { + return minDurationForQualityIncreaseUs; + } + if (chunkDurationUs != C.TIME_UNSET) { + // We are currently selecting a new live chunk. Even under perfect conditions, the buffered + // duration can't include the last chunk duration yet because we are still selecting a track + // for this or a previous chunk. Hence, we subtract one chunk duration from the total + // available live duration to ensure we only compare the buffered duration against what is + // actually achievable. + availableDurationUs -= chunkDurationUs; + } + return (long) (availableDurationUs * bufferedFractionToLiveEdgeForQualityIncrease); } /** diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelectionTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelectionTest.java index 8d38155b63..f2915110f9 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelectionTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelectionTest.java @@ -164,6 +164,40 @@ public final class AdaptiveTrackSelectionTest { assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); } + @Test + public void updateSelectedTrack_liveStream_switchesUpWhenBufferedFractionToLiveEdgeReached() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + // The second measurement onward returns 2000L, which prompts the track selection to switch up + // if possible. + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L, 2000L); + AdaptiveTrackSelection adaptiveTrackSelection = + prepareAdaptiveTrackSelectionWithBufferedFractionToLiveEdgeForQualiyIncrease( + trackGroup, /* bufferedFractionToLiveEdgeForQualityIncrease= */ 0.75f); + + // Not buffered close to live edge yet. + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 1_600_000, + /* availableDurationUs= */ 5_600_000, + /* queue= */ ImmutableList.of(), + createMediaChunkIterators(trackGroup, /* chunkDurationUs= */ 2_000_000)); + + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2); + + // Buffered all possible chunks (except for newly added chunk of 2 seconds). + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 3_600_000, + /* availableDurationUs= */ 5_600_000, + /* queue= */ ImmutableList.of(), + createMediaChunkIterators(trackGroup, /* chunkDurationUs= */ 2_000_000)); + + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format3); + } + @Test public void updateSelectedTrackDoNotSwitchDownIfBufferedEnough() { Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); @@ -732,6 +766,26 @@ public final class AdaptiveTrackSelectionTest { fakeClock)); } + private AdaptiveTrackSelection + prepareAdaptiveTrackSelectionWithBufferedFractionToLiveEdgeForQualiyIncrease( + TrackGroup trackGroup, float bufferedFractionToLiveEdgeForQualityIncrease) { + return prepareTrackSelection( + new AdaptiveTrackSelection( + trackGroup, + selectedAllTracksInGroup(trackGroup), + TrackSelection.TYPE_UNSET, + mockBandwidthMeter, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, + AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + AdaptiveTrackSelection.DEFAULT_MAX_WIDTH_TO_DISCARD, + AdaptiveTrackSelection.DEFAULT_MAX_HEIGHT_TO_DISCARD, + /* bandwidthFraction= */ 1.0f, + bufferedFractionToLiveEdgeForQualityIncrease, + /* adaptationCheckpoints= */ ImmutableList.of(), + fakeClock)); + } + private AdaptiveTrackSelection prepareAdaptiveTrackSelectionWithAdaptationCheckpoints( TrackGroup trackGroup, List adaptationCheckpoints) { return prepareTrackSelection(