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 1244b96d94..7bfd4c7cbe 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 @@ -246,7 +246,7 @@ public class DefaultLoadControl implements LoadControl { private final long backBufferDurationUs; private final boolean retainBackBufferFromKeyframe; - private int targetBufferSize; + private int targetBufferBytes; private boolean isBuffering; private boolean hasVideo; @@ -334,6 +334,10 @@ public class DefaultLoadControl implements LoadControl { this.bufferForPlaybackUs = C.msToUs(bufferForPlaybackMs); this.bufferForPlaybackAfterRebufferUs = C.msToUs(bufferForPlaybackAfterRebufferMs); this.targetBufferBytesOverwrite = targetBufferBytes; + this.targetBufferBytes = + targetBufferBytesOverwrite != C.LENGTH_UNSET + ? targetBufferBytesOverwrite + : DEFAULT_MUXED_BUFFER_SIZE; this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; this.backBufferDurationUs = C.msToUs(backBufferDurationMs); this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe; @@ -348,11 +352,11 @@ public class DefaultLoadControl implements LoadControl { public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { hasVideo = hasVideo(renderers, trackSelections); - targetBufferSize = + targetBufferBytes = targetBufferBytesOverwrite == C.LENGTH_UNSET - ? calculateTargetBufferSize(renderers, trackSelections) + ? calculateTargetBufferBytes(renderers, trackSelections) : targetBufferBytesOverwrite; - allocator.setTargetBufferSize(targetBufferSize); + allocator.setTargetBufferSize(targetBufferBytes); } @Override @@ -382,7 +386,7 @@ public class DefaultLoadControl implements LoadControl { @Override public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { - boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; + boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferBytes; long minBufferUs = hasVideo ? minBufferVideoUs : minBufferAudioUs; if (playbackSpeed > 1) { // The playback speed is faster than real time, so scale up the minimum required media @@ -391,6 +395,8 @@ public class DefaultLoadControl implements LoadControl { Util.getMediaDurationForPlayoutDuration(minBufferUs, playbackSpeed); minBufferUs = Math.min(mediaDurationMinBufferUs, maxBufferUs); } + // Prevent playback from getting stuck if minBufferUs is too small. + minBufferUs = Math.max(minBufferUs, 500_000); if (bufferedDurationUs < minBufferUs) { isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached; } else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) { @@ -407,7 +413,7 @@ public class DefaultLoadControl implements LoadControl { return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs || (!prioritizeTimeOverSizeThresholds - && allocator.getTotalBytesAllocated() >= targetBufferSize); + && allocator.getTotalBytesAllocated() >= targetBufferBytes); } /** @@ -418,7 +424,7 @@ public class DefaultLoadControl implements LoadControl { * @param trackSelectionArray The selected tracks. * @return The target buffer size in bytes. */ - protected int calculateTargetBufferSize( + protected int calculateTargetBufferBytes( Renderer[] renderers, TrackSelectionArray trackSelectionArray) { int targetBufferSize = 0; for (int i = 0; i < renderers.length; i++) { @@ -430,7 +436,10 @@ public class DefaultLoadControl implements LoadControl { } private void reset(boolean resetAllocator) { - targetBufferSize = 0; + targetBufferBytes = + targetBufferBytesOverwrite == C.LENGTH_UNSET + ? DEFAULT_MUXED_BUFFER_SIZE + : targetBufferBytesOverwrite; isBuffering = false; if (resetAllocator) { allocator.reset(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 547c6c3f25..2c6f7631cf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -707,6 +707,14 @@ import java.util.concurrent.atomic.AtomicBoolean; for (Renderer renderer : enabledRenderers) { renderer.maybeThrowStreamError(); } + if (!shouldContinueLoading + && playbackInfo.totalBufferedDurationUs < 500_000 + && isLoadingPossible()) { + // Throw if the LoadControl prevents loading even if the buffer is empty or almost empty. We + // can't compare against 0 to account for small differences between the renderer position + // and buffered position in the media at the point where playback gets stuck. + throw new IllegalStateException("Playback stuck buffering and not loading"); + } } if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY) @@ -1831,13 +1839,6 @@ import java.util.concurrent.atomic.AtomicBoolean; } long bufferedDurationUs = getTotalBufferedDurationUs(queue.getLoadingPeriod().getNextLoadPositionUs()); - if (bufferedDurationUs < 500_000) { - // Prevent loading from getting stuck even if LoadControl.shouldContinueLoading returns false - // when the buffer is empty or almost empty. We can't compare against 0 to account for small - // differences between the renderer position and buffered position in the media at the point - // where playback gets stuck. - return true; - } float playbackSpeed = mediaClock.getPlaybackParameters().speed; return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java index 31f432db15..2222d1a8d0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java @@ -46,6 +46,7 @@ public class DefaultLoadControlTest { @Test public void testShouldContinueLoading_untilMaxBufferExceeded() { createDefaultLoadControl(); + assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isTrue(); @@ -56,11 +57,27 @@ public class DefaultLoadControlTest { public void testShouldNotContinueLoadingOnceBufferingStopped_untilBelowMinBuffer() { createDefaultLoadControl(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); + assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue(); } + @Test + public void + testContinueLoadingOnceBufferingStopped_andBufferAlmostEmpty_evenIfMinBufferNotReached() { + builder.setBufferDurationsMs( + /* minBufferMs= */ 0, + /* maxBufferMs= */ (int) C.usToMs(MAX_BUFFER_US), + /* bufferForPlaybackMs= */ 0, + /* bufferForPlaybackAfterRebufferMs= */ 0); + createDefaultLoadControl(); + assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); + + assertThat(loadControl.shouldContinueLoading(5 * C.MICROS_PER_SECOND, SPEED)).isFalse(); + assertThat(loadControl.shouldContinueLoading(500L, SPEED)).isTrue(); + } + @Test public void testShouldContinueLoadingWithTargetBufferBytesReached_untilMinBufferReached() { createDefaultLoadControl(); @@ -81,6 +98,7 @@ public class DefaultLoadControlTest { makeSureTargetBufferBytesReached(); assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isFalse(); + assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); } @@ -91,7 +109,6 @@ public class DefaultLoadControlTest { // At normal playback speed, we stop buffering when the buffer reaches the minimum. assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); - // At double playback speed, we continue loading. assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, /* playbackSpeed= */ 2f)).isTrue(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index f48c33ebda..320854565d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -3150,14 +3150,54 @@ public final class ExoPlayerTest { } @Test - public void loadControlNeverWantsToLoadOrPlay_playbackDoesNotGetStuck() throws Exception { - LoadControl neverLoadingOrPlayingLoadControl = + public void loadControlNeverWantsToLoad_throwsIllegalStateException() throws Exception { + LoadControl neverLoadingLoadControl = new DefaultLoadControl() { @Override public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { return false; } + @Override + public boolean shouldStartPlayback( + long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { + return true; + } + }; + + // Use chunked data to ensure the player actually needs to continue loading and playing. + FakeAdaptiveDataSet.Factory dataSetFactory = + new FakeAdaptiveDataSet.Factory( + /* chunkDurationUs= */ 500_000, /* bitratePercentStdDev= */ 10.0); + MediaSource chunkedMediaSource = + new FakeAdaptiveMediaSource( + new FakeTimeline(/* windowCount= */ 1), + new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)), + new FakeChunkSource.Factory(dataSetFactory, new FakeDataSource.Factory())); + + try { + new ExoPlayerTestRunner.Builder() + .setLoadControl(neverLoadingLoadControl) + .setMediaSource(chunkedMediaSource) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + fail(); + } catch (ExoPlaybackException e) { + assertThat(e.type).isEqualTo(ExoPlaybackException.TYPE_UNEXPECTED); + assertThat(e.getUnexpectedException()).isInstanceOf(IllegalStateException.class); + } + } + + @Test + public void loadControlNeverWantsToPlay_playbackDoesNotGetStuck() throws Exception { + LoadControl neverLoadingOrPlayingLoadControl = + new DefaultLoadControl() { + @Override + public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { + return true; + } + @Override public boolean shouldStartPlayback( long bufferedDurationUs, float playbackSpeed, boolean rebuffering) {