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 7bfd4c7cbe..1244b96d94 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 targetBufferBytes; + private int targetBufferSize; private boolean isBuffering; private boolean hasVideo; @@ -334,10 +334,6 @@ 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; @@ -352,11 +348,11 @@ public class DefaultLoadControl implements LoadControl { public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { hasVideo = hasVideo(renderers, trackSelections); - targetBufferBytes = + targetBufferSize = targetBufferBytesOverwrite == C.LENGTH_UNSET - ? calculateTargetBufferBytes(renderers, trackSelections) + ? calculateTargetBufferSize(renderers, trackSelections) : targetBufferBytesOverwrite; - allocator.setTargetBufferSize(targetBufferBytes); + allocator.setTargetBufferSize(targetBufferSize); } @Override @@ -386,7 +382,7 @@ public class DefaultLoadControl implements LoadControl { @Override public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { - boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferBytes; + boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; long minBufferUs = hasVideo ? minBufferVideoUs : minBufferAudioUs; if (playbackSpeed > 1) { // The playback speed is faster than real time, so scale up the minimum required media @@ -395,8 +391,6 @@ 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) { @@ -413,7 +407,7 @@ public class DefaultLoadControl implements LoadControl { return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs || (!prioritizeTimeOverSizeThresholds - && allocator.getTotalBytesAllocated() >= targetBufferBytes); + && allocator.getTotalBytesAllocated() >= targetBufferSize); } /** @@ -424,7 +418,7 @@ public class DefaultLoadControl implements LoadControl { * @param trackSelectionArray The selected tracks. * @return The target buffer size in bytes. */ - protected int calculateTargetBufferBytes( + protected int calculateTargetBufferSize( Renderer[] renderers, TrackSelectionArray trackSelectionArray) { int targetBufferSize = 0; for (int i = 0; i < renderers.length; i++) { @@ -436,10 +430,7 @@ public class DefaultLoadControl implements LoadControl { } private void reset(boolean resetAllocator) { - targetBufferBytes = - targetBufferBytesOverwrite == C.LENGTH_UNSET - ? DEFAULT_MUXED_BUFFER_SIZE - : targetBufferBytesOverwrite; + targetBufferSize = 0; 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 47f85b603a..f2e78383e0 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,14 +830,6 @@ 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) @@ -1992,6 +1984,13 @@ 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 2222d1a8d0..31f432db15 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,7 +46,6 @@ 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(); @@ -57,27 +56,11 @@ 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(); @@ -98,7 +81,6 @@ 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(); } @@ -109,6 +91,7 @@ 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 0893e01ec0..ff03f09ff6 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 loadControlNeverWantsToLoad_throwsIllegalStateException() throws Exception { - LoadControl neverLoadingLoadControl = + public void loadControlNeverWantsToLoadOrPlay_playbackDoesNotGetStuck() throws Exception { + LoadControl neverLoadingOrPlayingLoadControl = 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 true; + return false; } }; @@ -3418,18 +3418,13 @@ public final class ExoPlayerTest { new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)), new FakeChunkSource.Factory(dataSetFactory, new FakeDataSource.Factory())); - 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); - } + new ExoPlayerTestRunner.Builder() + .setLoadControl(neverLoadingOrPlayingLoadControl) + .setMediaSources(chunkedMediaSource) + .build(context) + .start() + // This throws if playback doesn't finish within timeout. + .blockUntilEnded(TIMEOUT_MS); } @Test