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
This commit is contained in:
tonihei 2021-12-16 14:00:05 +00:00 committed by Ian Baker
parent 8bb53b409a
commit 9d463725fb
3 changed files with 73 additions and 5 deletions

View File

@ -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

View File

@ -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);
}
/**

View File

@ -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<AdaptationCheckpoint> adaptationCheckpoints) {
return prepareTrackSelection(