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 3068fbf471..ed3f41045a 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 @@ -830,6 +830,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) @@ -1991,13 +1999,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 ff03f09ff6..0893e01ec0 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 @@ -3393,8 +3393,8 @@ 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) { @@ -3404,7 +3404,7 @@ public final class ExoPlayerTest { @Override public boolean shouldStartPlayback( long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { - return false; + return true; } }; @@ -3418,13 +3418,18 @@ public final class ExoPlayerTest { new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)), new FakeChunkSource.Factory(dataSetFactory, new FakeDataSource.Factory())); - new ExoPlayerTestRunner.Builder() - .setLoadControl(neverLoadingOrPlayingLoadControl) - .setMediaSources(chunkedMediaSource) - .build(context) - .start() - // This throws if playback doesn't finish within timeout. - .blockUntilEnded(TIMEOUT_MS); + try { + new ExoPlayerTestRunner.Builder() + .setLoadControl(neverLoadingLoadControl) + .setMediaSources(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