*** Original commit ***

Rollback of d48dc4c159

*** Original commit ***

Move getting-stuck-prevention into DefaultLoadControl.

We recently added code that prevents getting stuck if the buffer is low and
the LoadControl refuses to continue loading (b84bde0252).

Move this logic into DefaultLoadControl to keep the workaround, and also apply the
maximum buffer size check in bytes if enabled. ExoPlayerImplInternal will now
throw if a...

***

PiperOrigin-RevId: 290273295
This commit is contained in:
tonihei 2020-01-17 16:15:28 +00:00 committed by Oliver Woodman
parent a7f7d8fb03
commit 3e41c0a1d2
4 changed files with 58 additions and 26 deletions

View file

@ -246,7 +246,7 @@ public class DefaultLoadControl implements LoadControl {
private final long backBufferDurationUs; private final long backBufferDurationUs;
private final boolean retainBackBufferFromKeyframe; private final boolean retainBackBufferFromKeyframe;
private int targetBufferSize; private int targetBufferBytes;
private boolean isBuffering; private boolean isBuffering;
private boolean hasVideo; private boolean hasVideo;
@ -334,6 +334,10 @@ public class DefaultLoadControl implements LoadControl {
this.bufferForPlaybackUs = C.msToUs(bufferForPlaybackMs); this.bufferForPlaybackUs = C.msToUs(bufferForPlaybackMs);
this.bufferForPlaybackAfterRebufferUs = C.msToUs(bufferForPlaybackAfterRebufferMs); this.bufferForPlaybackAfterRebufferUs = C.msToUs(bufferForPlaybackAfterRebufferMs);
this.targetBufferBytesOverwrite = targetBufferBytes; this.targetBufferBytesOverwrite = targetBufferBytes;
this.targetBufferBytes =
targetBufferBytesOverwrite != C.LENGTH_UNSET
? targetBufferBytesOverwrite
: DEFAULT_MUXED_BUFFER_SIZE;
this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;
this.backBufferDurationUs = C.msToUs(backBufferDurationMs); this.backBufferDurationUs = C.msToUs(backBufferDurationMs);
this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe; this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe;
@ -348,11 +352,11 @@ public class DefaultLoadControl implements LoadControl {
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
TrackSelectionArray trackSelections) { TrackSelectionArray trackSelections) {
hasVideo = hasVideo(renderers, trackSelections); hasVideo = hasVideo(renderers, trackSelections);
targetBufferSize = targetBufferBytes =
targetBufferBytesOverwrite == C.LENGTH_UNSET targetBufferBytesOverwrite == C.LENGTH_UNSET
? calculateTargetBufferSize(renderers, trackSelections) ? calculateTargetBufferBytes(renderers, trackSelections)
: targetBufferBytesOverwrite; : targetBufferBytesOverwrite;
allocator.setTargetBufferSize(targetBufferSize); allocator.setTargetBufferSize(targetBufferBytes);
} }
@Override @Override
@ -382,7 +386,7 @@ public class DefaultLoadControl implements LoadControl {
@Override @Override
public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferBytes;
long minBufferUs = hasVideo ? minBufferVideoUs : minBufferAudioUs; long minBufferUs = hasVideo ? minBufferVideoUs : minBufferAudioUs;
if (playbackSpeed > 1) { if (playbackSpeed > 1) {
// The playback speed is faster than real time, so scale up the minimum required media // The playback speed is faster than real time, so scale up the minimum required media
@ -391,6 +395,8 @@ public class DefaultLoadControl implements LoadControl {
Util.getMediaDurationForPlayoutDuration(minBufferUs, playbackSpeed); Util.getMediaDurationForPlayoutDuration(minBufferUs, playbackSpeed);
minBufferUs = Math.min(mediaDurationMinBufferUs, maxBufferUs); minBufferUs = Math.min(mediaDurationMinBufferUs, maxBufferUs);
} }
// Prevent playback from getting stuck if minBufferUs is too small.
minBufferUs = Math.max(minBufferUs, 500_000);
if (bufferedDurationUs < minBufferUs) { if (bufferedDurationUs < minBufferUs) {
isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached; isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached;
} else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) { } else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) {
@ -407,7 +413,7 @@ public class DefaultLoadControl implements LoadControl {
return minBufferDurationUs <= 0 return minBufferDurationUs <= 0
|| bufferedDurationUs >= minBufferDurationUs || bufferedDurationUs >= minBufferDurationUs
|| (!prioritizeTimeOverSizeThresholds || (!prioritizeTimeOverSizeThresholds
&& allocator.getTotalBytesAllocated() >= targetBufferSize); && allocator.getTotalBytesAllocated() >= targetBufferBytes);
} }
/** /**
@ -418,7 +424,7 @@ public class DefaultLoadControl implements LoadControl {
* @param trackSelectionArray The selected tracks. * @param trackSelectionArray The selected tracks.
* @return The target buffer size in bytes. * @return The target buffer size in bytes.
*/ */
protected int calculateTargetBufferSize( protected int calculateTargetBufferBytes(
Renderer[] renderers, TrackSelectionArray trackSelectionArray) { Renderer[] renderers, TrackSelectionArray trackSelectionArray) {
int targetBufferSize = 0; int targetBufferSize = 0;
for (int i = 0; i < renderers.length; i++) { for (int i = 0; i < renderers.length; i++) {
@ -430,7 +436,10 @@ public class DefaultLoadControl implements LoadControl {
} }
private void reset(boolean resetAllocator) { private void reset(boolean resetAllocator) {
targetBufferSize = 0; targetBufferBytes =
targetBufferBytesOverwrite == C.LENGTH_UNSET
? DEFAULT_MUXED_BUFFER_SIZE
: targetBufferBytesOverwrite;
isBuffering = false; isBuffering = false;
if (resetAllocator) { if (resetAllocator) {
allocator.reset(); allocator.reset();

View file

@ -830,6 +830,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
for (Renderer renderer : enabledRenderers) { for (Renderer renderer : enabledRenderers) {
renderer.maybeThrowStreamError(); renderer.maybeThrowStreamError();
} }
if (!shouldContinueLoading
&& 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");
}
} }
if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY) if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY)
@ -1991,13 +1999,6 @@ 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);
} }

View file

@ -46,6 +46,7 @@ public class DefaultLoadControlTest {
@Test @Test
public void testShouldContinueLoading_untilMaxBufferExceeded() { public void testShouldContinueLoading_untilMaxBufferExceeded() {
createDefaultLoadControl(); createDefaultLoadControl();
assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue();
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isTrue();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isTrue();
@ -56,11 +57,27 @@ public class DefaultLoadControlTest {
public void testShouldNotContinueLoadingOnceBufferingStopped_untilBelowMinBuffer() { public void testShouldNotContinueLoadingOnceBufferingStopped_untilBelowMinBuffer() {
createDefaultLoadControl(); createDefaultLoadControl();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isFalse();
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse();
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue();
} }
@Test
public void
testContinueLoadingOnceBufferingStopped_andBufferAlmostEmpty_evenIfMinBufferNotReached() {
builder.setBufferDurationsMs(
/* minBufferMs= */ 0,
/* maxBufferMs= */ (int) C.usToMs(MAX_BUFFER_US),
/* bufferForPlaybackMs= */ 0,
/* bufferForPlaybackAfterRebufferMs= */ 0);
createDefaultLoadControl();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();
assertThat(loadControl.shouldContinueLoading(5 * C.MICROS_PER_SECOND, SPEED)).isFalse();
assertThat(loadControl.shouldContinueLoading(500L, SPEED)).isTrue();
}
@Test @Test
public void testShouldContinueLoadingWithTargetBufferBytesReached_untilMinBufferReached() { public void testShouldContinueLoadingWithTargetBufferBytesReached_untilMinBufferReached() {
createDefaultLoadControl(); createDefaultLoadControl();
@ -81,6 +98,7 @@ public class DefaultLoadControlTest {
makeSureTargetBufferBytesReached(); makeSureTargetBufferBytesReached();
assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isFalse();
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isFalse();
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();
} }
@ -91,7 +109,6 @@ public class DefaultLoadControlTest {
// At normal playback speed, we stop buffering when the buffer reaches the minimum. // At normal playback speed, we stop buffering when the buffer reaches the minimum.
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse();
// At double playback speed, we continue loading. // At double playback speed, we continue loading.
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, /* playbackSpeed= */ 2f)).isTrue(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, /* playbackSpeed= */ 2f)).isTrue();
} }

View file

@ -3393,8 +3393,8 @@ public final class ExoPlayerTest {
} }
@Test @Test
public void loadControlNeverWantsToLoadOrPlay_playbackDoesNotGetStuck() throws Exception { public void loadControlNeverWantsToLoad_throwsIllegalStateException() throws Exception {
LoadControl neverLoadingOrPlayingLoadControl = LoadControl neverLoadingLoadControl =
new DefaultLoadControl() { new DefaultLoadControl() {
@Override @Override
public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {
@ -3404,7 +3404,7 @@ public final class ExoPlayerTest {
@Override @Override
public boolean shouldStartPlayback( public boolean shouldStartPlayback(
long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { long bufferedDurationUs, float playbackSpeed, boolean rebuffering) {
return false; return true;
} }
}; };
@ -3418,13 +3418,18 @@ public final class ExoPlayerTest {
new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)), new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)),
new FakeChunkSource.Factory(dataSetFactory, new FakeDataSource.Factory())); new FakeChunkSource.Factory(dataSetFactory, new FakeDataSource.Factory()));
new ExoPlayerTestRunner.Builder() try {
.setLoadControl(neverLoadingOrPlayingLoadControl) new ExoPlayerTestRunner.Builder()
.setMediaSources(chunkedMediaSource) .setLoadControl(neverLoadingLoadControl)
.build(context) .setMediaSources(chunkedMediaSource)
.start() .build(context)
// This throws if playback doesn't finish within timeout. .start()
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
fail();
} catch (ExoPlaybackException e) {
assertThat(e.type).isEqualTo(ExoPlaybackException.TYPE_UNEXPECTED);
assertThat(e.getUnexpectedException()).isInstanceOf(IllegalStateException.class);
}
} }
@Test @Test