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 77219c4397..a88335b0ff 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 @@ -1817,6 +1817,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/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 8bd6b1ba09..f17cdae56b 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 @@ -52,6 +52,10 @@ import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; +import com.google.android.exoplayer2.testutil.FakeAdaptiveDataSet; +import com.google.android.exoplayer2.testutil.FakeAdaptiveMediaSource; +import com.google.android.exoplayer2.testutil.FakeChunkSource; +import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; import com.google.android.exoplayer2.testutil.FakeMediaPeriod; import com.google.android.exoplayer2.testutil.FakeMediaSource; @@ -3143,6 +3147,41 @@ public final class ExoPlayerTest { testRunner.blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); } + @Test + public void loadControlNeverWantsToLoadOrPlay_playbackDoesNotGetStuck() throws Exception { + LoadControl neverLoadingOrPlayingLoadControl = + new DefaultLoadControl() { + @Override + public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { + return false; + } + + @Override + public boolean shouldStartPlayback( + long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { + return false; + } + }; + + // Use chunked data to ensure the player actually needs to continue loading and playing. + FakeAdaptiveDataSet.Factory dataSetFactory = + new FakeAdaptiveDataSet.Factory( + /* chunkDurationUs= */ 500_000, /* bitratePercentStdDev= */ 10.0); + MediaSource chunkedMediaSource = + new FakeAdaptiveMediaSource( + new FakeTimeline(/* windowCount= */ 1), + new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)), + new FakeChunkSource.Factory(dataSetFactory, new FakeDataSource.Factory())); + + new ExoPlayerTestRunner.Builder() + .setLoadControl(neverLoadingOrPlayingLoadControl) + .setMediaSource(chunkedMediaSource) + .build(context) + .start() + // This throws if playback doesn't finish within timeout. + .blockUntilEnded(TIMEOUT_MS); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index bf3cc90a78..8fe6d9b6c9 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -56,18 +56,34 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc */ public static final class Builder { - /** - * A generic video {@link Format} which can be used to set up media sources and renderers. - */ - public static final Format VIDEO_FORMAT = Format.createVideoSampleFormat(null, - MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE, - null, null); + /** A generic video {@link Format} which can be used to set up media sources and renderers. */ + public static final Format VIDEO_FORMAT = + Format.createVideoSampleFormat( + /* id= */ null, + /* sampleMimeType= */ MimeTypes.VIDEO_H264, + /* codecs= */ null, + /* bitrate= */ 800_000, + /* maxInputSize= */ Format.NO_VALUE, + /* width= */ 1280, + /* height= */ 720, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null); - /** - * A generic audio {@link Format} which can be used to set up media sources and renderers. - */ - public static final Format AUDIO_FORMAT = Format.createAudioSampleFormat(null, - MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); + /** A generic audio {@link Format} which can be used to set up media sources and renderers. */ + public static final Format AUDIO_FORMAT = + Format.createAudioSampleFormat( + /* id= */ null, + /* sampleMimeType= */ MimeTypes.AUDIO_AAC, + /* codecs= */ null, + /* bitrate= */ 100_000, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 2, + /* sampleRate= */ 44100, + /* initializationData=*/ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); private Clock clock; private Timeline timeline;