Commit b84bde02 by tonihei Committed by bachinger

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
parent 4f37d28e
...@@ -1817,6 +1817,13 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1817,6 +1817,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
long bufferedDurationUs = long bufferedDurationUs =
getTotalBufferedDurationUs(queue.getLoadingPeriod().getNextLoadPositionUs()); 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; float playbackSpeed = mediaClock.getPlaybackParameters().speed;
return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed);
} }
......
...@@ -52,6 +52,10 @@ import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; ...@@ -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.AutoAdvancingFakeClock;
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner;
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; 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.FakeMediaClockRenderer;
import com.google.android.exoplayer2.testutil.FakeMediaPeriod; import com.google.android.exoplayer2.testutil.FakeMediaPeriod;
import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeMediaSource;
...@@ -3143,6 +3147,41 @@ public final class ExoPlayerTest { ...@@ -3143,6 +3147,41 @@ public final class ExoPlayerTest {
testRunner.blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); 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. // Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
......
...@@ -56,18 +56,34 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -56,18 +56,34 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
*/ */
public static final class Builder { public static final class Builder {
/** /** A generic video {@link Format} which can be used to set up media sources and renderers. */
* A generic video {@link Format} which can be used to set up media sources and renderers. public static final Format VIDEO_FORMAT =
*/ Format.createVideoSampleFormat(
public static final Format VIDEO_FORMAT = Format.createVideoSampleFormat(null, /* id= */ null,
MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE, /* sampleMimeType= */ MimeTypes.VIDEO_H264,
null, null); /* codecs= */ null,
/* bitrate= */ 800_000,
/** /* maxInputSize= */ Format.NO_VALUE,
* A generic audio {@link Format} which can be used to set up media sources and renderers. /* width= */ 1280,
*/ /* height= */ 720,
public static final Format AUDIO_FORMAT = Format.createAudioSampleFormat(null, /* frameRate= */ Format.NO_VALUE,
MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); /* 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(
/* 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 Clock clock;
private Timeline timeline; private Timeline timeline;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment