From b84bde025258e7307c52eaf6bbe58157d788aa06 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 3 Dec 2019 11:54:04 +0000 Subject: [PATCH] Prevent stuck playback if shouldContinueLoading returns false If LoadControl.shouldContinueLoading returns false and the renderers are not ready for playback using the already buffered data, playback is stuck. To prevent this situation, we always continue loading if the buffer is almost empty. We already have a similar workaround for when LoadControl.shouldStartPlayback returns false even if loading stopped. Having both workarounds allows playback to continue even if the LoadControl tries to prevent loading and playing all the time. PiperOrigin-RevId: 283516750 --- .../exoplayer2/ExoPlayerImplInternal.java | 7 ++++ .../android/exoplayer2/ExoPlayerTest.java | 39 +++++++++++++++++++ .../testutil/ExoPlayerTestRunner.java | 38 ++++++++++++------ 3 files changed, 73 insertions(+), 11 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 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;