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 7677740f5d..55cd5fcea2 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 @@ -167,6 +167,11 @@ import java.util.concurrent.atomic.AtomicBoolean; * to load it. */ private static final long PLAYBACK_STUCK_AFTER_MS = 4000; + /** + * Threshold under which a buffered duration is assumed to be empty. We cannot use zero to account + * for buffers currently hold but not played by the renderer. + */ + private static final long PLAYBACK_BUFFER_EMPTY_THRESHOLD_US = 500_000; private final Renderer[] renderers; private final Set renderersToReset; @@ -1050,7 +1055,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } } if (!playbackInfo.isLoading - && playbackInfo.totalBufferedDurationUs < 500_000 + && playbackInfo.totalBufferedDurationUs < PLAYBACK_BUFFER_EMPTY_THRESHOLD_US && isLoadingPossible()) { // The renderers are not ready, there is more media available to load, and the LoadControl // is refusing to load it (indicated by !playbackInfo.isLoading). This could be because the @@ -2306,8 +2311,23 @@ import java.util.concurrent.atomic.AtomicBoolean; ? loadingPeriodHolder.toPeriodTime(rendererPositionUs) : loadingPeriodHolder.toPeriodTime(rendererPositionUs) - loadingPeriodHolder.info.startPositionUs; - return loadControl.shouldContinueLoading( - playbackPositionUs, bufferedDurationUs, mediaClock.getPlaybackParameters().speed); + boolean shouldContinueLoading = + loadControl.shouldContinueLoading( + playbackPositionUs, bufferedDurationUs, mediaClock.getPlaybackParameters().speed); + if (!shouldContinueLoading + && bufferedDurationUs < PLAYBACK_BUFFER_EMPTY_THRESHOLD_US + && (backBufferDurationUs > 0 || retainBackBufferFromKeyframe)) { + // LoadControl doesn't want to continue loading despite no buffered data. Clear back buffer + // and try again in case it's blocked on memory usage of the back buffer. + queue + .getPlayingPeriod() + .mediaPeriod + .discardBuffer(playbackInfo.positionUs, /* toKeyframe= */ false); + shouldContinueLoading = + loadControl.shouldContinueLoading( + playbackPositionUs, bufferedDurationUs, mediaClock.getPlaybackParameters().speed); + } + return shouldContinueLoading; } private boolean isLoadingPossible() { 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 6394bba782..e4c733fd88 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 @@ -12085,6 +12085,26 @@ public final class ExoPlayerTest { verify(listener, atLeast(2)).onDeviceVolumeChanged(anyInt(), anyBoolean()); } + @Test + public void loadControlBackBuffer_withInsufficientMemoryLimits_stillContinuesPlayback() + throws Exception { + DefaultLoadControl loadControl = + new DefaultLoadControl.Builder() + .setTargetBufferBytes(500_000) + .setBackBuffer( + /* backBufferDurationMs= */ 1_000_000, /* retainBackBufferFromKeyframe= */ true) + .build(); + + ExoPlayer player = new TestExoPlayerBuilder(context).setLoadControl(loadControl).build(); + player.setMediaItem( + MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); + player.prepare(); + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); + + // Assert that playing works without getting stuck due to the memory used by the back buffer. + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {