diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5f03454544..fa7fea5868 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,6 +21,9 @@ * Add `MediaCodecAdapter.getMetrics()` to allow users obtain metrics data from `MediaCodec`. ([#9766](https://github.com/google/ExoPlayer/issues/9766)). + * Amend logic in `AdaptiveTrackSelection` to allow a quality increase + under sufficient network bandwidth even if playback is very close to the + live edge ((#9784)[https://github.com/google/ExoPlayer/issues/9784]). * Android 12 compatibility: * Upgrade the Cast extension to depend on `com.google.android.gms:play-services-cast-framework:20.1.0`. Earlier diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index 918888dff6..5c95b0c320 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -458,8 +458,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; @@ -599,13 +601,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/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java index 8059d0731e..6b453b5f5a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java @@ -163,6 +163,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); @@ -731,6 +765,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(