*** 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 LoadControl lets playback get stuck. This includes the case where
DefaultLoadControl reaches its maximum buffer size and not even a mim...

***

PiperOrigin-RevId: 286071115
This commit is contained in:
olly 2019-12-17 23:18:33 +00:00 committed by Oliver Woodman
parent 36fa9d5a43
commit 3c56b113e4
4 changed files with 26 additions and 58 deletions

View File

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

View File

@ -830,14 +830,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
for (Renderer renderer : enabledRenderers) {
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)
@ -1992,6 +1984,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);
}

View File

@ -46,7 +46,6 @@ public class DefaultLoadControlTest {
@Test
public void testShouldContinueLoading_untilMaxBufferExceeded() {
createDefaultLoadControl();
assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue();
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isTrue();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isTrue();
@ -57,27 +56,11 @@ public class DefaultLoadControlTest {
public void testShouldNotContinueLoadingOnceBufferingStopped_untilBelowMinBuffer() {
createDefaultLoadControl();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, 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 - 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
public void testShouldContinueLoadingWithTargetBufferBytesReached_untilMinBufferReached() {
createDefaultLoadControl();
@ -98,7 +81,6 @@ public class DefaultLoadControlTest {
makeSureTargetBufferBytesReached();
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(MAX_BUFFER_US, SPEED)).isFalse();
}
@ -109,6 +91,7 @@ public class DefaultLoadControlTest {
// At normal playback speed, we stop buffering when the buffer reaches the minimum.
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse();
// At double playback speed, we continue loading.
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, /* playbackSpeed= */ 2f)).isTrue();
}

View File

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