From 8df931b1fea00c341c58495f342bc156029c6905 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 7 Sep 2022 10:09:29 +0000 Subject: [PATCH] Discard backbuffer before playback gets stuck. If the back buffer is using too much memory, there is a risk playback could get stuck because LoadControl refuses to load further data. This eventually results in a stuck-buffering playback error. We can detect this case, clear the back buffer and then ask the LoadControl again to avoid failing playback in such a case. PiperOrigin-RevId: 472679797 (cherry picked from commit 125646e4c98e80b35d1a8b195300d324a9aaf2b5) --- .../exoplayer2/ExoPlayerImplInternal.java | 26 ++++++++++++++++--- .../android/exoplayer2/ExoPlayerTest.java | 20 ++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) 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) {