From 9d463725fb33523e9c15fae8cf0d7ac3c86e9d6c Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 16 Dec 2021 14:00:05 +0000 Subject: [PATCH] Exclude last chunk when applying fraction for live quality increase We check the fraction of the available duration we have already buffered for live streams to see if we can increase the quality. This fraction compares against the overall available media duration at the time of the track selection, which by definition can't include one of the availabe chunks (as this is the one we want to load next). That means, for example, that for a reasonable live offset of 3 segments we can at most reach a fraction of 0.66, which is less than our default threshold of 0.75, meaning we can never switch up. By subtracting one chunk duration from the available duration, we make this comparison fair again and allow all live streams (regardless of live offset) to reach up to 100% buffered data (which is above our default value of 75%), so that they can increase the quality. Issue: google/ExoPlayer#9784 PiperOrigin-RevId: 416791033 --- RELEASENOTES.md | 3 ++ .../AdaptiveTrackSelection.java | 21 ++++++-- .../AdaptiveTrackSelectionTest.java | 54 +++++++++++++++++++ 3 files changed, 73 insertions(+), 5 deletions(-) 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(