diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java deleted file mode 100644 index b850a08aeb..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java +++ /dev/null @@ -1,494 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.trackselection; - -import android.util.Pair; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.chunk.MediaChunk; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.trackselection.TrackSelection.Definition; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.upstream.DefaultAllocator; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Clock; -import java.util.List; -import org.checkerframework.checker.nullness.compatqual.NullableType; - -/** - * Builder for a {@link TrackSelection.Factory} and {@link LoadControl} that implement buffer size - * based track adaptation. - */ -public final class BufferSizeAdaptationBuilder { - - /** Dynamic filter for formats, which is applied when selecting a new track. */ - public interface DynamicFormatFilter { - - /** Filter which allows all formats. */ - DynamicFormatFilter NO_FILTER = (format, trackBitrate, isInitialSelection) -> true; - - /** - * Called when updating the selected track to determine whether a candidate track is allowed. If - * no format is allowed or eligible, the lowest quality format will be used. - * - * @param format The {@link Format} of the candidate track. - * @param trackBitrate The estimated bitrate of the track. May differ from {@link - * Format#bitrate} if a more accurate estimate of the current track bitrate is available. - * @param isInitialSelection Whether this is for the initial track selection. - */ - boolean isFormatAllowed(Format format, int trackBitrate, boolean isInitialSelection); - } - - /** - * The default minimum duration of media that the player will attempt to ensure is buffered at all - * times, in milliseconds. - */ - public static final int DEFAULT_MIN_BUFFER_MS = 15000; - - /** - * The default maximum duration of media that the player will attempt to buffer, in milliseconds. - */ - public static final int DEFAULT_MAX_BUFFER_MS = 50000; - - /** - * The default duration of media that must be buffered for playback to start or resume following a - * user action such as a seek, in milliseconds. - */ - public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; - - /** - * The default duration of media that must be buffered for playback to resume after a rebuffer, in - * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action. - */ - public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; - - /** - * The default offset the current duration of buffered media must deviate from the ideal duration - * of buffered media for the currently selected format, before the selected format is changed. - */ - public static final int DEFAULT_HYSTERESIS_BUFFER_MS = 5000; - - /** - * During start-up phase, the default fraction of the available bandwidth that the selection - * should consider available for use. Setting to a value less than 1 is recommended to account for - * inaccuracies in the bandwidth estimator. - */ - public static final float DEFAULT_START_UP_BANDWIDTH_FRACTION = - AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION; - - /** - * During start-up phase, the default minimum duration of buffered media required for the selected - * track to switch to one of higher quality based on measured bandwidth. - */ - public static final int DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS = - AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS; - - @Nullable private DefaultAllocator allocator; - private Clock clock; - private int minBufferMs; - private int maxBufferMs; - private int bufferForPlaybackMs; - private int bufferForPlaybackAfterRebufferMs; - private int hysteresisBufferMs; - private float startUpBandwidthFraction; - private int startUpMinBufferForQualityIncreaseMs; - private DynamicFormatFilter dynamicFormatFilter; - private boolean buildCalled; - - /** Creates builder with default values. */ - public BufferSizeAdaptationBuilder() { - clock = Clock.DEFAULT; - minBufferMs = DEFAULT_MIN_BUFFER_MS; - maxBufferMs = DEFAULT_MAX_BUFFER_MS; - bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS; - bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; - hysteresisBufferMs = DEFAULT_HYSTERESIS_BUFFER_MS; - startUpBandwidthFraction = DEFAULT_START_UP_BANDWIDTH_FRACTION; - startUpMinBufferForQualityIncreaseMs = DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS; - dynamicFormatFilter = DynamicFormatFilter.NO_FILTER; - } - - /** - * Set the clock to use. Should only be set for testing purposes. - * - * @param clock The {@link Clock}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setClock(Clock clock) { - Assertions.checkState(!buildCalled); - this.clock = clock; - return this; - } - - /** - * Sets the {@link DefaultAllocator} used by the loader. - * - * @param allocator The {@link DefaultAllocator}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setAllocator(DefaultAllocator allocator) { - Assertions.checkState(!buildCalled); - this.allocator = allocator; - return this; - } - - /** - * Sets the buffer duration parameters. - * - * @param minBufferMs The minimum duration of media that the player will attempt to ensure is - * buffered at all times, in milliseconds. - * @param maxBufferMs The maximum duration of media that the player will attempt to buffer, in - * milliseconds. - * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or - * resume following a user action such as a seek, in milliseconds. - * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for - * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by - * buffer depletion rather than a user action. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setBufferDurationsMs( - int minBufferMs, - int maxBufferMs, - int bufferForPlaybackMs, - int bufferForPlaybackAfterRebufferMs) { - Assertions.checkState(!buildCalled); - this.minBufferMs = minBufferMs; - this.maxBufferMs = maxBufferMs; - this.bufferForPlaybackMs = bufferForPlaybackMs; - this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs; - return this; - } - - /** - * Sets the hysteresis buffer used to prevent repeated format switching. - * - * @param hysteresisBufferMs The offset the current duration of buffered media must deviate from - * the ideal duration of buffered media for the currently selected format, before the selected - * format is changed. This value must be smaller than {@code maxBufferMs - minBufferMs}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setHysteresisBufferMs(int hysteresisBufferMs) { - Assertions.checkState(!buildCalled); - this.hysteresisBufferMs = hysteresisBufferMs; - return this; - } - - /** - * Sets track selection parameters used during the start-up phase before the selection can be made - * purely on based on buffer size. During the start-up phase the selection is based on the current - * bandwidth estimate. - * - * @param bandwidthFraction The fraction of the available bandwidth that the selection should - * consider available for use. Setting to a value less than 1 is recommended to account for - * inaccuracies in the bandwidth estimator. - * @param minBufferForQualityIncreaseMs The minimum duration of buffered media required for the - * selected track to switch to one of higher quality. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setStartUpTrackSelectionParameters( - float bandwidthFraction, int minBufferForQualityIncreaseMs) { - Assertions.checkState(!buildCalled); - this.startUpBandwidthFraction = bandwidthFraction; - this.startUpMinBufferForQualityIncreaseMs = minBufferForQualityIncreaseMs; - return this; - } - - /** - * Sets the {@link DynamicFormatFilter} to use when updating the selected track. - * - * @param dynamicFormatFilter The {@link DynamicFormatFilter}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setDynamicFormatFilter( - DynamicFormatFilter dynamicFormatFilter) { - Assertions.checkState(!buildCalled); - this.dynamicFormatFilter = dynamicFormatFilter; - return this; - } - - /** - * Builds player components for buffer size based track adaptation. - * - * @return A pair of a {@link TrackSelection.Factory} and a {@link LoadControl}, which should be - * used to construct the player. - */ - public Pair buildPlayerComponents() { - Assertions.checkArgument(hysteresisBufferMs < maxBufferMs - minBufferMs); - Assertions.checkState(!buildCalled); - buildCalled = true; - - DefaultLoadControl.Builder loadControlBuilder = - new DefaultLoadControl.Builder() - .setTargetBufferBytes(/* targetBufferBytes = */ Integer.MAX_VALUE) - .setBufferDurationsMs( - /* minBufferMs= */ maxBufferMs, - maxBufferMs, - bufferForPlaybackMs, - bufferForPlaybackAfterRebufferMs); - if (allocator != null) { - loadControlBuilder.setAllocator(allocator); - } - - TrackSelection.Factory trackSelectionFactory = - new TrackSelection.Factory() { - @Override - public @NullableType TrackSelection[] createTrackSelections( - @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { - return TrackSelectionUtil.createTrackSelectionsForDefinitions( - definitions, - definition -> - new BufferSizeAdaptiveTrackSelection( - definition.group, - definition.tracks, - bandwidthMeter, - minBufferMs, - maxBufferMs, - hysteresisBufferMs, - startUpBandwidthFraction, - startUpMinBufferForQualityIncreaseMs, - dynamicFormatFilter, - clock)); - } - }; - - return Pair.create(trackSelectionFactory, loadControlBuilder.createDefaultLoadControl()); - } - - private static final class BufferSizeAdaptiveTrackSelection extends BaseTrackSelection { - - private static final int BITRATE_BLACKLISTED = Format.NO_VALUE; - - private final BandwidthMeter bandwidthMeter; - private final Clock clock; - private final DynamicFormatFilter dynamicFormatFilter; - private final int[] formatBitrates; - private final long minBufferUs; - private final long maxBufferUs; - private final long hysteresisBufferUs; - private final float startUpBandwidthFraction; - private final long startUpMinBufferForQualityIncreaseUs; - private final int minBitrate; - private final int maxBitrate; - private final double bitrateToBufferFunctionSlope; - private final double bitrateToBufferFunctionIntercept; - - private boolean isInSteadyState; - private int selectedIndex; - private int selectionReason; - private float playbackSpeed; - - private BufferSizeAdaptiveTrackSelection( - TrackGroup trackGroup, - int[] tracks, - BandwidthMeter bandwidthMeter, - int minBufferMs, - int maxBufferMs, - int hysteresisBufferMs, - float startUpBandwidthFraction, - int startUpMinBufferForQualityIncreaseMs, - DynamicFormatFilter dynamicFormatFilter, - Clock clock) { - super(trackGroup, tracks); - this.bandwidthMeter = bandwidthMeter; - this.minBufferUs = C.msToUs(minBufferMs); - this.maxBufferUs = C.msToUs(maxBufferMs); - this.hysteresisBufferUs = C.msToUs(hysteresisBufferMs); - this.startUpBandwidthFraction = startUpBandwidthFraction; - this.startUpMinBufferForQualityIncreaseUs = C.msToUs(startUpMinBufferForQualityIncreaseMs); - this.dynamicFormatFilter = dynamicFormatFilter; - this.clock = clock; - - formatBitrates = new int[length]; - maxBitrate = getFormat(/* index= */ 0).bitrate; - minBitrate = getFormat(/* index= */ length - 1).bitrate; - selectionReason = C.SELECTION_REASON_UNKNOWN; - playbackSpeed = 1.0f; - - // We use a log-linear function to map from bitrate to buffer size: - // buffer = slope * ln(bitrate) + intercept, - // with buffer(minBitrate) = minBuffer and buffer(maxBitrate) = maxBuffer - hysteresisBuffer. - bitrateToBufferFunctionSlope = - (maxBufferUs - hysteresisBufferUs - minBufferUs) - / Math.log((double) maxBitrate / minBitrate); - bitrateToBufferFunctionIntercept = - minBufferUs - bitrateToBufferFunctionSlope * Math.log(minBitrate); - } - - @Override - public void onPlaybackSpeed(float playbackSpeed) { - this.playbackSpeed = playbackSpeed; - } - - @Override - public void onDiscontinuity() { - isInSteadyState = false; - } - - @Override - public int getSelectedIndex() { - return selectedIndex; - } - - @Override - public int getSelectionReason() { - return selectionReason; - } - - @Override - @Nullable - public Object getSelectionData() { - return null; - } - - @Override - public void updateSelectedTrack( - long playbackPositionUs, - long bufferedDurationUs, - long availableDurationUs, - List queue, - MediaChunkIterator[] mediaChunkIterators) { - updateFormatBitrates(/* nowMs= */ clock.elapsedRealtime()); - - // Make initial selection - if (selectionReason == C.SELECTION_REASON_UNKNOWN) { - selectionReason = C.SELECTION_REASON_INITIAL; - selectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ true); - return; - } - - long bufferUs = getCurrentPeriodBufferedDurationUs(playbackPositionUs, bufferedDurationUs); - int oldSelectedIndex = selectedIndex; - if (isInSteadyState) { - selectIndexSteadyState(bufferUs); - } else { - selectIndexStartUpPhase(bufferUs); - } - if (selectedIndex != oldSelectedIndex) { - selectionReason = C.SELECTION_REASON_ADAPTIVE; - } - } - - // Steady state. - - private void selectIndexSteadyState(long bufferUs) { - if (isOutsideHysteresis(bufferUs)) { - selectedIndex = selectIdealIndexUsingBufferSize(bufferUs); - } - } - - private boolean isOutsideHysteresis(long bufferUs) { - if (formatBitrates[selectedIndex] == BITRATE_BLACKLISTED) { - return true; - } - long targetBufferForCurrentBitrateUs = - getTargetBufferForBitrateUs(formatBitrates[selectedIndex]); - long bufferDiffUs = bufferUs - targetBufferForCurrentBitrateUs; - return Math.abs(bufferDiffUs) > hysteresisBufferUs; - } - - private int selectIdealIndexUsingBufferSize(long bufferUs) { - int lowestBitrateNonBlacklistedIndex = 0; - for (int i = 0; i < formatBitrates.length; i++) { - if (formatBitrates[i] != BITRATE_BLACKLISTED) { - if (getTargetBufferForBitrateUs(formatBitrates[i]) <= bufferUs - && dynamicFormatFilter.isFormatAllowed( - getFormat(i), formatBitrates[i], /* isInitialSelection= */ false)) { - return i; - } - lowestBitrateNonBlacklistedIndex = i; - } - } - return lowestBitrateNonBlacklistedIndex; - } - - // Startup. - - private void selectIndexStartUpPhase(long bufferUs) { - int startUpSelectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ false); - int steadyStateSelectedIndex = selectIdealIndexUsingBufferSize(bufferUs); - if (steadyStateSelectedIndex <= selectedIndex) { - // Switch to steady state if we have enough buffer to maintain current selection. - selectedIndex = steadyStateSelectedIndex; - isInSteadyState = true; - } else { - if (bufferUs < startUpMinBufferForQualityIncreaseUs - && startUpSelectedIndex < selectedIndex - && formatBitrates[selectedIndex] != BITRATE_BLACKLISTED) { - // Switching up from a non-blacklisted track is only allowed if we have enough buffer. - return; - } - selectedIndex = startUpSelectedIndex; - } - } - - private int selectIdealIndexUsingBandwidth(boolean isInitialSelection) { - long effectiveBitrate = - (long) (bandwidthMeter.getBitrateEstimate() * startUpBandwidthFraction); - int lowestBitrateNonBlacklistedIndex = 0; - for (int i = 0; i < formatBitrates.length; i++) { - if (formatBitrates[i] != BITRATE_BLACKLISTED) { - if (Math.round(formatBitrates[i] * playbackSpeed) <= effectiveBitrate - && dynamicFormatFilter.isFormatAllowed( - getFormat(i), formatBitrates[i], isInitialSelection)) { - return i; - } - lowestBitrateNonBlacklistedIndex = i; - } - } - return lowestBitrateNonBlacklistedIndex; - } - - // Utility methods. - - private void updateFormatBitrates(long nowMs) { - for (int i = 0; i < length; i++) { - if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) { - formatBitrates[i] = getFormat(i).bitrate; - } else { - formatBitrates[i] = BITRATE_BLACKLISTED; - } - } - } - - private long getTargetBufferForBitrateUs(int bitrate) { - if (bitrate <= minBitrate) { - return minBufferUs; - } - if (bitrate >= maxBitrate) { - return maxBufferUs - hysteresisBufferUs; - } - return (int) - (bitrateToBufferFunctionSlope * Math.log(bitrate) + bitrateToBufferFunctionIntercept); - } - - private static long getCurrentPeriodBufferedDurationUs( - long playbackPositionUs, long bufferedDurationUs) { - return playbackPositionUs >= 0 ? bufferedDurationUs : playbackPositionUs + bufferedDurationUs; - } - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java deleted file mode 100644 index 8b20630a23..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.trackselection; - -import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -import android.util.Pair; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.util.MimeTypes; -import java.util.Collections; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; - -/** Unit test for the track selection created by {@link BufferSizeAdaptationBuilder}. */ -@RunWith(AndroidJUnit4.class) -public final class BufferSizeAdaptiveTrackSelectionTest { - - private static final int MIN_BUFFER_MS = 15_000; - private static final int MAX_BUFFER_MS = 50_000; - private static final int HYSTERESIS_BUFFER_MS = 10_000; - private static final float BANDWIDTH_FRACTION = 0.5f; - private static final int MIN_BUFFER_FOR_QUALITY_INCREASE_MS = 10_000; - - /** - * Factor between bitrates is always the same (=2.2). That means buffer levels should be linearly - * distributed between MIN_BUFFER=15s and MAX_BUFFER-HYSTERESIS=50s-10s=40s. - */ - private static final Format format1 = - createVideoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); - - private static final Format format2 = - createVideoFormat(/* bitrate= */ 1100, /* width= */ 640, /* height= */ 480); - private static final Format format3 = - createVideoFormat(/* bitrate= */ 2420, /* width= */ 960, /* height= */ 720); - private static final int BUFFER_LEVEL_FORMAT_2 = - (MIN_BUFFER_MS + MAX_BUFFER_MS - HYSTERESIS_BUFFER_MS) / 2; - private static final int BUFFER_LEVEL_FORMAT_3 = MAX_BUFFER_MS - HYSTERESIS_BUFFER_MS; - - @Mock private BandwidthMeter mockBandwidthMeter; - private TrackSelection trackSelection; - - @Before - public void setUp() { - initMocks(this); - Pair trackSelectionFactoryAndLoadControl = - new BufferSizeAdaptationBuilder() - .setBufferDurationsMs( - MIN_BUFFER_MS, - MAX_BUFFER_MS, - /* bufferForPlaybackMs= */ 1000, - /* bufferForPlaybackAfterRebufferMs= */ 1000) - .setHysteresisBufferMs(HYSTERESIS_BUFFER_MS) - .setStartUpTrackSelectionParameters( - BANDWIDTH_FRACTION, MIN_BUFFER_FOR_QUALITY_INCREASE_MS) - .buildPlayerComponents(); - trackSelection = - trackSelectionFactoryAndLoadControl - .first - .createTrackSelections( - new TrackSelection.Definition[] { - new TrackSelection.Definition( - new TrackGroup(format1, format2, format3), /* tracks= */ 0, 1, 2) - }, - mockBandwidthMeter)[0]; - trackSelection.enable(); - } - - @Test - public void updateSelectedTrack_usesBandwidthEstimateForInitialSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - - updateSelectedTrack(/* bufferedDurationMs= */ 0); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void updateSelectedTrack_withLowerBandwidthEstimateDuringStartUp_switchesDown() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ 0); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void - updateSelectedTrack_withHigherBandwidthEstimateDuringStartUp_andLowBuffer_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3)); - - updateSelectedTrack(/* bufferedDurationMs= */ MIN_BUFFER_FOR_QUALITY_INCREASE_MS - 1); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void - updateSelectedTrack_withHigherBandwidthEstimateDuringStartUp_andHighBuffer_switchesUp() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3)); - - updateSelectedTrack(/* bufferedDurationMs= */ MIN_BUFFER_FOR_QUALITY_INCREASE_MS); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format3); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void - updateSelectedTrack_withIncreasedBandwidthEstimate_onceSteadyStateBufferIsReached_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3)); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void - updateSelectedTrack_withDecreasedBandwidthEstimate_onceSteadyStateBufferIsReached_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void updateSelectedTrack_withIncreasedBufferInSteadyState_switchesUp() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_3); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format3); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void updateSelectedTrack_withDecreasedBufferInSteadyState_switchesDown() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - HYSTERESIS_BUFFER_MS - 1); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void - updateSelectedTrack_withDecreasedBufferInSteadyState_withinHysteresis_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - HYSTERESIS_BUFFER_MS); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void onDiscontinuity_switchesBackToStartUpState() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - trackSelection.onDiscontinuity(); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - 1); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - private void updateSelectedTrack(long bufferedDurationMs) { - trackSelection.updateSelectedTrack( - /* playbackPositionUs= */ 0, - /* bufferedDurationUs= */ C.msToUs(bufferedDurationMs), - /* availableDurationUs= */ C.TIME_UNSET, - /* queue= */ Collections.emptyList(), - /* mediaChunkIterators= */ new MediaChunkIterator[] { - MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY - }); - } - - private static Format createVideoFormat(int bitrate, int width, int height) { - return Format.createVideoSampleFormat( - /* id= */ null, - /* sampleMimeType= */ MimeTypes.VIDEO_H264, - /* codecs= */ null, - /* bitrate= */ bitrate, - /* maxInputSize= */ Format.NO_VALUE, - /* width= */ width, - /* height= */ height, - /* frameRate= */ Format.NO_VALUE, - /* initializationData= */ null, - /* drmInitData= */ null); - } - - private static long getBitrateEstimateEnoughFor(Format format) { - return (long) (format.bitrate / BANDWIDTH_FRACTION) + 1; - } -}