diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index 653ca997e2..baec1a3f7b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -175,6 +175,13 @@ import java.util.concurrent.atomic.AtomicBoolean; * this does not matter for now. */ private static final long MIN_RENDERER_SLEEP_DURATION_MS = 2000; + /** + * Duration for which the player needs to appear stuck before the playback is failed on the + * assumption that no further progress will be made. To appear stuck, the player's renderers must + * not be ready, there must be more media available to load, and the LoadControl must be refusing + * to load it. + */ + private static final long PLAYBACK_STUCK_AFTER_MS = 4000; private final Renderer[] renderers; private final Set renderersToReset; @@ -214,15 +221,14 @@ import java.util.concurrent.atomic.AtomicBoolean; private boolean foregroundMode; private boolean requestForRendererSleep; private boolean offloadSchedulingEnabled; - private int enabledRendererCount; @Nullable private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; private int nextPendingMessageIndexHint; private boolean deliverPendingMessageAtStartPositionRequired; @Nullable private ExoPlaybackException pendingRecoverableRendererError; - private long setForegroundModeTimeoutMs; + private long playbackMaybeBecameStuckAtMs; public ExoPlayerImplInternal( Renderer[] renderers, @@ -256,6 +262,7 @@ import java.util.concurrent.atomic.AtomicBoolean; this.pauseAtEndOfWindow = pauseAtEndOfWindow; this.clock = clock; + playbackMaybeBecameStuckAtMs = C.TIME_UNSET; backBufferDurationUs = loadControl.getBackBufferDurationUs(); retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); @@ -676,6 +683,9 @@ import java.util.concurrent.atomic.AtomicBoolean; private void setState(int state) { if (playbackInfo.playbackState != state) { + if (state != Player.STATE_BUFFERING) { + playbackMaybeBecameStuckAtMs = C.TIME_UNSET; + } playbackInfo = playbackInfo.copyWithPlaybackState(state); } } @@ -1046,6 +1056,7 @@ import java.util.concurrent.atomic.AtomicBoolean; stopRenderers(); } + boolean playbackMaybeStuck = false; if (playbackInfo.playbackState == Player.STATE_BUFFERING) { for (int i = 0; i < renderers.length; i++) { if (isRendererEnabled(renderers[i]) @@ -1056,12 +1067,24 @@ import java.util.concurrent.atomic.AtomicBoolean; if (!playbackInfo.isLoading && 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"); + // 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 + // renderers are still transitioning to their ready states, but it could also indicate a + // stuck playback. The playbackInfo.totalBufferedDurationUs check further isolates the + // cause to a lack of media for the renderers to consume, to avoid classifying playbacks as + // stuck when they're waiting for other reasons (in particular, loading DRM keys). + playbackMaybeStuck = true; } } + + if (!playbackMaybeStuck) { + playbackMaybeBecameStuckAtMs = C.TIME_UNSET; + } else if (playbackMaybeBecameStuckAtMs == C.TIME_UNSET) { + playbackMaybeBecameStuckAtMs = clock.elapsedRealtime(); + } else if (clock.elapsedRealtime() - playbackMaybeBecameStuckAtMs >= PLAYBACK_STUCK_AFTER_MS) { + throw new IllegalStateException("Playback stuck buffering and not loading"); + } + if (offloadSchedulingEnabled != playbackInfo.offloadSchedulingEnabled) { playbackInfo = playbackInfo.copyWithOffloadSchedulingEnabled(offloadSchedulingEnabled); }