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
This commit is contained in:
tonihei 2022-09-07 10:09:29 +00:00 committed by Marc Baechinger
parent c401fb9771
commit 310e0fec5c
3 changed files with 45 additions and 3 deletions

View File

@ -11,6 +11,8 @@
([#10057](https://github.com/google/ExoPlayer/issues/10057)).
* Limit parallel download removals to 1 to avoid excessive thread creation
([#10458](https://github.com/google/ExoPlayer/issues/10458)).
* Discard back buffer before playback gets stuck due to insufficient
available memory.
* Downloads:
* Fix potential infinite loop in `ProgressiveDownloader` caused by
simultaneous download and playback with the same `PriorityTaskManager`

View File

@ -175,6 +175,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<Renderer> renderersToReset;
@ -1058,7 +1063,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
@ -2314,8 +2319,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() {

View File

@ -12095,6 +12095,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) {