diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index e8ea2f1621..dca2fa53a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.PriorityTaskManager; import com.google.android.exoplayer2.util.Util; @@ -60,6 +61,116 @@ public class DefaultLoadControl implements LoadControl { /** The default prioritization of buffer time constraints over size constraints. */ public static final boolean DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS = true; + /** Builder for {@link DefaultLoadControl}. */ + public static final class Builder { + + private DefaultAllocator allocator; + private int minBufferMs; + private int maxBufferMs; + private int bufferForPlaybackMs; + private int bufferForPlaybackAfterRebufferMs; + private int targetBufferBytes; + private boolean prioritizeTimeOverSizeThresholds; + private PriorityTaskManager priorityTaskManager; + + /** Constructs a new instance. */ + public Builder() { + allocator = null; + minBufferMs = DEFAULT_MIN_BUFFER_MS; + maxBufferMs = DEFAULT_MAX_BUFFER_MS; + bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS; + bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; + targetBufferBytes = DEFAULT_TARGET_BUFFER_BYTES; + prioritizeTimeOverSizeThresholds = DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS; + priorityTaskManager = null; + } + + /** + * Sets the {@link DefaultAllocator} used by the loader. + * + * @param allocator The {@link DefaultAllocator}. + * @return This builder, for convenience. + */ + public Builder setAllocator(DefaultAllocator allocator) { + 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. + */ + public Builder setBufferDurationsMs( + int minBufferMs, + int maxBufferMs, + int bufferForPlaybackMs, + int bufferForPlaybackAfterRebufferMs) { + this.minBufferMs = minBufferMs; + this.maxBufferMs = maxBufferMs; + this.bufferForPlaybackMs = bufferForPlaybackMs; + this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs; + return this; + } + + /** + * Sets the target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the target buffer + * size will be calculated using {@link #calculateTargetBufferSize(Renderer[], + * TrackSelectionArray)}. + * + * @param targetBufferBytes The target buffer size in bytes. + * @return This builder, for convenience. + */ + public Builder setTargetBufferBytes(int targetBufferBytes) { + this.targetBufferBytes = targetBufferBytes; + return this; + } + + /** + * Sets whether the load control prioritizes buffer time constraints over buffer size + * constraints. + * + * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time + * constraints over buffer size constraints. + * @return This builder, for convenience. + */ + public Builder setPrioritizeTimeOverSizeThresholds(boolean prioritizeTimeOverSizeThresholds) { + this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; + return this; + } + + /** Sets the {@link PriorityTaskManager} to use. */ + public Builder setPriorityTaskManager(PriorityTaskManager priorityTaskManager) { + this.priorityTaskManager = priorityTaskManager; + return this; + } + + /** Creates a {@link DefaultLoadControl}. */ + public DefaultLoadControl createDefaultLoadControl() { + if (allocator == null) { + allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); + } + return new DefaultLoadControl( + allocator, + minBufferMs, + maxBufferMs, + bufferForPlaybackMs, + bufferForPlaybackAfterRebufferMs, + targetBufferBytes, + prioritizeTimeOverSizeThresholds, + priorityTaskManager); + } + } + private final DefaultAllocator allocator; private final long minBufferUs; @@ -80,11 +191,8 @@ public class DefaultLoadControl implements LoadControl { this(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE)); } - /** - * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. - * - * @param allocator The {@link DefaultAllocator} used by the loader. - */ + /** @deprecated Use {@link Builder} instead. */ + @Deprecated public DefaultLoadControl(DefaultAllocator allocator) { this( allocator, @@ -96,24 +204,8 @@ public class DefaultLoadControl implements LoadControl { DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS); } - /** - * Constructs a new instance. - * - * @param allocator The {@link DefaultAllocator} used by the loader. - * @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 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. - * @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the - * target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[], - * TrackSelectionArray)}. - * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time - */ + /** @deprecated Use {@link Builder} instead. */ + @Deprecated public DefaultLoadControl( DefaultAllocator allocator, int minBufferMs, @@ -133,27 +225,8 @@ public class DefaultLoadControl implements LoadControl { null); } - /** - * Constructs a new instance. - * - * @param allocator The {@link DefaultAllocator} used by the loader. - * @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 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. - * @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the - * target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[], - * TrackSelectionArray)}. - * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time - * constraints over buffer size constraints. - * @param priorityTaskManager If not null, registers itself as a task with priority {@link - * C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining - */ + /** @deprecated Use {@link Builder} instead. */ + @Deprecated public DefaultLoadControl( DefaultAllocator allocator, int minBufferMs, @@ -163,6 +236,16 @@ public class DefaultLoadControl implements LoadControl { int targetBufferBytes, boolean prioritizeTimeOverSizeThresholds, PriorityTaskManager priorityTaskManager) { + assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0"); + assertGreaterOrEqual(bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackMs", "0"); + assertGreaterOrEqual(minBufferMs, bufferForPlaybackMs, "minBufferMs", "bufferForPlaybackMs"); + assertGreaterOrEqual( + minBufferMs, + bufferForPlaybackAfterRebufferMs, + "minBufferMs", + "bufferForPlaybackAfterRebufferMs"); + assertGreaterOrEqual(maxBufferMs, minBufferMs, "maxBufferMs", "minBufferMs"); + this.allocator = allocator; minBufferUs = minBufferMs * 1000L; maxBufferUs = maxBufferMs * 1000L; @@ -217,18 +300,11 @@ public class DefaultLoadControl implements LoadControl { public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean wasBuffering = isBuffering; - if (prioritizeTimeOverSizeThresholds) { - isBuffering = - bufferedDurationUs < minBufferUs // below low watermark - || (bufferedDurationUs <= maxBufferUs // between watermarks - && isBuffering - && !targetBufferSizeReached); - } else { - isBuffering = - !targetBufferSizeReached - && (bufferedDurationUs < minBufferUs // below low watermark - || (bufferedDurationUs <= maxBufferUs && isBuffering)); // between watermarks - } + if (bufferedDurationUs < minBufferUs) { + isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached; + } else if (bufferedDurationUs > maxBufferUs || targetBufferSizeReached) { + isBuffering = false; + } // Else don't change the buffering state if (priorityTaskManager != null && isBuffering != wasBuffering) { if (isBuffering) { priorityTaskManager.add(C.PRIORITY_PLAYBACK); @@ -280,4 +356,7 @@ public class DefaultLoadControl implements LoadControl { } } + private static void assertGreaterOrEqual(int value1, int value2, String name1, String name2) { + Assertions.checkArgument(value1 >= value2, name1 + " cannot be less than " + name2); + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlShouldContinueLoadingTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlShouldContinueLoadingTest.java new file mode 100644 index 0000000000..21215e97bf --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlShouldContinueLoadingTest.java @@ -0,0 +1,111 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.DefaultLoadControl.Builder; +import com.google.android.exoplayer2.upstream.DefaultAllocator; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link DefaultLoadControl#shouldContinueLoading(long, float)}. */ +@RunWith(RobolectricTestRunner.class) +public class DefaultLoadControlShouldContinueLoadingTest { + + private static final int PLAYBACK_SPEED = 1; + private static final int MIN_BUFFER_MS = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS; + private static final int MAX_BUFFER_MS = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS; + private static final int TARGET_BUFFER_BYTES = C.DEFAULT_BUFFER_SEGMENT_SIZE * 2; + private Builder builder; + private DefaultAllocator allocator; + private DefaultLoadControl defaultLoadControl; + + @Before + public void setUp() throws Exception { + builder = new Builder(); + allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); + } + + @Test + public void testReturnsTrueUntilMaximumBufferExceeded() throws Exception { + createDefaultLoadControl(); + assertThatShouldContinueLoadingReturnsTrue(0); + assertThatShouldContinueLoadingReturnsTrue(MIN_BUFFER_MS); + assertThatShouldContinueLoadingReturnsTrue(MAX_BUFFER_MS); + assertThatShouldContinueLoadingReturnsFalse(MAX_BUFFER_MS + 1); + } + + @Test + public void testReturnsFalseUntilFallBelowMinimumBufferOnceBufferingStopped() throws Exception { + createDefaultLoadControl(); + assertThatShouldContinueLoadingReturnsFalse(MAX_BUFFER_MS + 1); + assertThatShouldContinueLoadingReturnsFalse(MAX_BUFFER_MS); + assertThatShouldContinueLoadingReturnsFalse(MIN_BUFFER_MS); + assertThatShouldContinueLoadingReturnsTrue(MIN_BUFFER_MS - 1); + } + + @Test + public void testReturnsTrueUntilMinimumBufferReachedAlthoughTargetBufferBytesReached() + throws Exception { + createDefaultLoadControl(); + makeSureTargetBufferBytesReached(); + + assertThatShouldContinueLoadingReturnsTrue(0); + assertThatShouldContinueLoadingReturnsTrue(MIN_BUFFER_MS - 1); + assertThatShouldContinueLoadingReturnsFalse(MIN_BUFFER_MS); + assertThatShouldContinueLoadingReturnsFalse(MAX_BUFFER_MS + 1); + } + + @Test + public void testAlwaysReturnsFalseIfMaximumBufferReachedAndNotPrioritizeTimeOverSizeThresholds() + throws Exception { + builder.setPrioritizeTimeOverSizeThresholds(false); + createDefaultLoadControl(); + // Put defaultLoadControl in buffering state. + assertThatShouldContinueLoadingReturnsTrue(0); + makeSureTargetBufferBytesReached(); + + assertThatShouldContinueLoadingReturnsFalse(0); + assertThatShouldContinueLoadingReturnsFalse(MIN_BUFFER_MS); + assertThatShouldContinueLoadingReturnsFalse(MAX_BUFFER_MS + 1); + } + + private void createDefaultLoadControl() { + builder.setAllocator(allocator); + builder.setTargetBufferBytes(TARGET_BUFFER_BYTES); + defaultLoadControl = builder.createDefaultLoadControl(); + defaultLoadControl.onTracksSelected(new Renderer[0], null, null); + } + + private void makeSureTargetBufferBytesReached() { + while (allocator.getTotalBytesAllocated() < TARGET_BUFFER_BYTES) { + allocator.allocate(); + } + } + + private void assertThatShouldContinueLoadingReturnsFalse(int bufferedDurationMs) { + assertThat(defaultLoadControl.shouldContinueLoading(bufferedDurationMs * 1000, PLAYBACK_SPEED)) + .isFalse(); + } + + private void assertThatShouldContinueLoadingReturnsTrue(int bufferedDurationMs) { + assertThat(defaultLoadControl.shouldContinueLoading(bufferedDurationMs * 1000, PLAYBACK_SPEED)) + .isTrue(); + } +}