Add basic multi-player support to DefaultLoadControl

This change makes sure the `DefaultLoadControl` would work
when passed to multiple players. It makes sure and unit tests
that the loading state of a player is maintained for each player
that is using `DefaultLoadControl`.

The targetBufferSize of the `DefaultAllocator` is increased
linearly for each player and memory is allocated in a simple
first-come-first-serve manner.

PiperOrigin-RevId: 622126523
This commit is contained in:
bachinger 2024-04-05 03:24:02 -07:00 committed by Copybara-Service
parent e0fa697edf
commit 08cc6e673d
3 changed files with 450 additions and 89 deletions

View File

@ -15,23 +15,27 @@
*/ */
package androidx.media3.exoplayer; package androidx.media3.exoplayer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Timeline; import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.DefaultAllocator; import androidx.media3.exoplayer.upstream.DefaultAllocator;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.HashMap;
/** The default {@link LoadControl} implementation. */ /** The default {@link LoadControl} implementation. */
@UnstableApi @UnstableApi
@ -183,8 +187,10 @@ public class DefaultLoadControl implements LoadControl {
} }
/** /**
* Sets the target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the target buffer * Sets the target buffer size in bytes for each player. The actual overall target buffer size
* size will be calculated based on the selected tracks. * is this value multiplied by the number of players that use the load control simultaneously.
* If set to {@link C#LENGTH_UNSET}, the target buffer size of a player will be calculated based
* on the selected tracks of the player.
* *
* @param targetBufferBytes The target buffer size in bytes. * @param targetBufferBytes The target buffer size in bytes.
* @return This builder, for convenience. * @return This builder, for convenience.
@ -262,9 +268,9 @@ public class DefaultLoadControl implements LoadControl {
private final boolean prioritizeTimeOverSizeThresholds; private final boolean prioritizeTimeOverSizeThresholds;
private final long backBufferDurationUs; private final long backBufferDurationUs;
private final boolean retainBackBufferFromKeyframe; private final boolean retainBackBufferFromKeyframe;
private final HashMap<PlayerId, PlayerLoadingState> loadingStates;
private int targetBufferBytes; private long threadId;
private boolean isLoading;
/** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */ /** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */
public DefaultLoadControl() { public DefaultLoadControl() {
@ -308,42 +314,53 @@ public class DefaultLoadControl implements LoadControl {
this.bufferForPlaybackUs = Util.msToUs(bufferForPlaybackMs); this.bufferForPlaybackUs = Util.msToUs(bufferForPlaybackMs);
this.bufferForPlaybackAfterRebufferUs = Util.msToUs(bufferForPlaybackAfterRebufferMs); this.bufferForPlaybackAfterRebufferUs = Util.msToUs(bufferForPlaybackAfterRebufferMs);
this.targetBufferBytesOverwrite = targetBufferBytes; this.targetBufferBytesOverwrite = targetBufferBytes;
this.targetBufferBytes =
targetBufferBytesOverwrite != C.LENGTH_UNSET
? targetBufferBytesOverwrite
: DEFAULT_MIN_BUFFER_SIZE;
this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;
this.backBufferDurationUs = Util.msToUs(backBufferDurationMs); this.backBufferDurationUs = Util.msToUs(backBufferDurationMs);
this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe; this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe;
loadingStates = new HashMap<>();
threadId = C.INDEX_UNSET;
} }
@Override @Override
public void onPrepared() { public void onPrepared(PlayerId playerId) {
reset(false); long currentThreadId = Thread.currentThread().getId();
checkState(
threadId == C.INDEX_UNSET || threadId == currentThreadId,
"Players that share the same LoadControl must share the same playback thread. See"
+ " ExoPlayer.Builder.setPlaybackLooper(Looper).");
threadId = currentThreadId;
if (!loadingStates.containsKey(playerId)) {
loadingStates.put(playerId, new PlayerLoadingState());
}
resetPlayerLoadingState(playerId);
} }
@Override @Override
public void onTracksSelected( public void onTracksSelected(
PlayerId playerId,
Timeline timeline, Timeline timeline,
MediaPeriodId mediaPeriodId, MediaPeriodId mediaPeriodId,
Renderer[] renderers, Renderer[] renderers,
TrackGroupArray trackGroups, TrackGroupArray trackGroups,
ExoTrackSelection[] trackSelections) { ExoTrackSelection[] trackSelections) {
targetBufferBytes = checkNotNull(loadingStates.get(playerId)).targetBufferBytes =
targetBufferBytesOverwrite == C.LENGTH_UNSET targetBufferBytesOverwrite == C.LENGTH_UNSET
? calculateTargetBufferBytes(renderers, trackSelections) ? calculateTargetBufferBytes(renderers, trackSelections)
: targetBufferBytesOverwrite; : targetBufferBytesOverwrite;
allocator.setTargetBufferSize(targetBufferBytes); updateAllocator();
} }
@Override @Override
public void onStopped() { public void onStopped(PlayerId playerId) {
reset(true); removePlayer(playerId);
} }
@Override @Override
public void onReleased() { public void onReleased(PlayerId playerId) {
reset(true); removePlayer(playerId);
if (loadingStates.isEmpty()) {
threadId = C.INDEX_UNSET;
}
} }
@Override @Override
@ -352,19 +369,26 @@ public class DefaultLoadControl implements LoadControl {
} }
@Override @Override
public long getBackBufferDurationUs() { public long getBackBufferDurationUs(PlayerId playerId) {
return backBufferDurationUs; return backBufferDurationUs;
} }
@Override @Override
public boolean retainBackBufferFromKeyframe() { public boolean retainBackBufferFromKeyframe(PlayerId playerId) {
return retainBackBufferFromKeyframe; return retainBackBufferFromKeyframe;
} }
@Override @Override
public boolean shouldContinueLoading( public boolean shouldContinueLoading(
long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) { PlayerId playerId,
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferBytes; Timeline timeline,
MediaPeriodId mediaPeriodId,
long playbackPositionUs,
long bufferedDurationUs,
float playbackSpeed) {
PlayerLoadingState playerLoadingState = checkNotNull(loadingStates.get(playerId));
boolean targetBufferSizeReached =
allocator.getTotalBytesAllocated() >= calculateTotalTargetBufferBytes();
long minBufferUs = this.minBufferUs; long minBufferUs = this.minBufferUs;
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
@ -376,20 +400,21 @@ public class DefaultLoadControl implements LoadControl {
// Prevent playback from getting stuck if minBufferUs is too small. // Prevent playback from getting stuck if minBufferUs is too small.
minBufferUs = max(minBufferUs, 500_000); minBufferUs = max(minBufferUs, 500_000);
if (bufferedDurationUs < minBufferUs) { if (bufferedDurationUs < minBufferUs) {
isLoading = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached; playerLoadingState.isLoading = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached;
if (!isLoading && bufferedDurationUs < 500_000) { if (!playerLoadingState.isLoading && bufferedDurationUs < 500_000) {
Log.w( Log.w(
"DefaultLoadControl", "DefaultLoadControl",
"Target buffer size reached with less than 500ms of buffered media data."); "Target buffer size reached with less than 500ms of buffered media data.");
} }
} else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) { } else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) {
isLoading = false; playerLoadingState.isLoading = false;
} // Else don't change the loading state. } // Else don't change the loading state.
return isLoading; return playerLoadingState.isLoading;
} }
@Override @Override
public boolean shouldStartPlayback( public boolean shouldStartPlayback(
PlayerId playerId,
Timeline timeline, Timeline timeline,
MediaPeriodId mediaPeriodId, MediaPeriodId mediaPeriodId,
long bufferedDurationUs, long bufferedDurationUs,
@ -404,7 +429,7 @@ public class DefaultLoadControl implements LoadControl {
return minBufferDurationUs <= 0 return minBufferDurationUs <= 0
|| bufferedDurationUs >= minBufferDurationUs || bufferedDurationUs >= minBufferDurationUs
|| (!prioritizeTimeOverSizeThresholds || (!prioritizeTimeOverSizeThresholds
&& allocator.getTotalBytesAllocated() >= targetBufferBytes); && allocator.getTotalBytesAllocated() >= calculateTotalTargetBufferBytes());
} }
/** /**
@ -426,14 +451,35 @@ public class DefaultLoadControl implements LoadControl {
return max(DEFAULT_MIN_BUFFER_SIZE, targetBufferSize); return max(DEFAULT_MIN_BUFFER_SIZE, targetBufferSize);
} }
private void reset(boolean resetAllocator) { @VisibleForTesting
targetBufferBytes = /* package */ int calculateTotalTargetBufferBytes() {
int totalTargetBufferBytes = 0;
for (PlayerLoadingState state : loadingStates.values()) {
totalTargetBufferBytes += state.targetBufferBytes;
}
return totalTargetBufferBytes;
}
private void resetPlayerLoadingState(PlayerId playerId) {
PlayerLoadingState playerLoadingState = checkNotNull(loadingStates.get(playerId));
playerLoadingState.targetBufferBytes =
targetBufferBytesOverwrite == C.LENGTH_UNSET targetBufferBytesOverwrite == C.LENGTH_UNSET
? DEFAULT_MIN_BUFFER_SIZE ? DEFAULT_MIN_BUFFER_SIZE
: targetBufferBytesOverwrite; : targetBufferBytesOverwrite;
isLoading = false; playerLoadingState.isLoading = false;
if (resetAllocator) { }
private void removePlayer(PlayerId playerId) {
if (loadingStates.remove(playerId) != null) {
updateAllocator();
}
}
private void updateAllocator() {
if (loadingStates.isEmpty()) {
allocator.reset(); allocator.reset();
} else {
allocator.setTargetBufferSize(calculateTotalTargetBufferBytes());
} }
} }
@ -464,4 +510,9 @@ public class DefaultLoadControl implements LoadControl {
private static void assertGreaterOrEqual(int value1, int value2, String name1, String name2) { private static void assertGreaterOrEqual(int value1, int value2, String name1, String name2) {
Assertions.checkArgument(value1 >= value2, name1 + " cannot be less than " + name2); Assertions.checkArgument(value1 >= value2, name1 + " cannot be less than " + name2);
} }
private static class PlayerLoadingState {
public boolean isLoading;
public int targetBufferBytes;
}
} }

View File

@ -43,7 +43,8 @@ public final class DefaultAllocator implements Allocator {
* Constructs an instance without creating any {@link Allocation}s up front. * Constructs an instance without creating any {@link Allocation}s up front.
* *
* @param trimOnReset Whether memory is freed when the allocator is reset. Should be true unless * @param trimOnReset Whether memory is freed when the allocator is reset. Should be true unless
* the allocator will be re-used by multiple player instances. * the allocator will be re-used by multiple player instances. If set to false, trimming can
* be forced by calling {@link #setTargetBufferSize(int)} manually when required.
* @param individualAllocationSize The length of each individual {@link Allocation}. * @param individualAllocationSize The length of each individual {@link Allocation}.
*/ */
public DefaultAllocator(boolean trimOnReset, int individualAllocationSize) { public DefaultAllocator(boolean trimOnReset, int individualAllocationSize) {
@ -56,7 +57,8 @@ public final class DefaultAllocator implements Allocator {
* <p>Note: {@link Allocation}s created up front will never be discarded by {@link #trim()}. * <p>Note: {@link Allocation}s created up front will never be discarded by {@link #trim()}.
* *
* @param trimOnReset Whether memory is freed when the allocator is reset. Should be true unless * @param trimOnReset Whether memory is freed when the allocator is reset. Should be true unless
* the allocator will be re-used by multiple player instances. * the allocator will be re-used by multiple player instances. If set to false, trimming can
* be forced by calling {@link #setTargetBufferSize(int)} manually when required.
* @param individualAllocationSize The length of each individual {@link Allocation}. * @param individualAllocationSize The length of each individual {@link Allocation}.
* @param initialAllocationCount The number of allocations to create up front. * @param initialAllocationCount The number of allocations to create up front.
*/ */

View File

@ -18,12 +18,22 @@ package androidx.media3.exoplayer;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.Timeline; import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.DefaultLoadControl.Builder; import androidx.media3.exoplayer.DefaultLoadControl.Builder;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.SinglePeriodTimeline;
import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.trackselection.FixedTrackSelection;
import androidx.media3.exoplayer.upstream.DefaultAllocator; import androidx.media3.exoplayer.upstream.DefaultAllocator;
import androidx.media3.test.utils.FakeRenderer;
import androidx.media3.test.utils.FakeTimeline;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -41,27 +51,126 @@ public class DefaultLoadControlTest {
private Builder builder; private Builder builder;
private DefaultAllocator allocator; private DefaultAllocator allocator;
private DefaultLoadControl loadControl; private DefaultLoadControl loadControl;
private PlayerId playerId;
private Timeline timeline;
private MediaSource.MediaPeriodId mediaPeriodId;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
builder = new Builder(); builder = new Builder();
allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
playerId =
Util.SDK_INT < 31
? new PlayerId(/* playerName= */ "")
: new PlayerId(/* logSessionId= */ null, /* playerName= */ "");
timeline =
new SinglePeriodTimeline(
/* durationUs= */ 10_000_000L,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* useLiveConfiguration= */ false,
/* manifest= */ null,
MediaItem.EMPTY);
mediaPeriodId =
new MediaSource.MediaPeriodId(
timeline.getPeriod(/* periodIndex= */ 0, new Timeline.Period()));
} }
@Test @Test
public void shouldContinueLoading_untilMaxBufferExceeded() { public void shouldContinueLoading_untilMaxBufferExceeded() {
build(); build();
assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
/* bufferedDurationUs= */ 0L,
SPEED))
.isTrue();
assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US - 1,
SPEED))
.isTrue();
assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US,
SPEED))
.isFalse();
}
@Test
public void shouldContinueLoading_twoPlayers_loadingStatesAreSeparated() {
builder.setBufferDurationsMs(
/* minBufferMs= */ (int) Util.usToMs(MIN_BUFFER_US),
/* maxBufferMs= */ (int) Util.usToMs(MAX_BUFFER_US),
/* bufferForPlaybackMs= */ 0,
/* bufferForPlaybackAfterRebufferMs= */ 0);
build();
// A second player uses the load control.
PlayerId playerId2 = new PlayerId(/* playerName= */ "");
Timeline timeline2 = new FakeTimeline();
MediaSource.MediaPeriodId mediaPeriodId2 =
new MediaSource.MediaPeriodId(
timeline.getPeriod(/* periodIndex= */ 0, new Timeline.Period()));
loadControl.onPrepared(playerId2);
// First player is fully buffered. Buffer starts depleting until it falls under min size.
loadControl.shouldContinueLoading(
playerId, timeline, mediaPeriodId, /* playbackPositionUs= */ 0L, MAX_BUFFER_US, SPEED);
// Second player fell below min size and starts loading until max size is reached.
loadControl.shouldContinueLoading(
playerId2,
timeline2,
mediaPeriodId2,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US - 1,
SPEED);
assertThat( assertThat(
loadControl.shouldContinueLoading( loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, SPEED)) playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US - 1,
SPEED))
.isFalse();
assertThat(
loadControl.shouldContinueLoading(
playerId2,
timeline2,
mediaPeriodId2,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US,
SPEED))
.isTrue(); .isTrue();
assertThat( assertThat(
loadControl.shouldContinueLoading( loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, MAX_BUFFER_US - 1, SPEED)) playerId,
.isTrue(); timeline,
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED)) mediaPeriodId,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US,
SPEED))
.isFalse(); .isFalse();
assertThat(
loadControl.shouldContinueLoading(
playerId2,
timeline2,
mediaPeriodId2,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US - 1,
SPEED))
.isTrue();
} }
@Test @Test
@ -73,17 +182,41 @@ public class DefaultLoadControlTest {
/* bufferForPlaybackAfterRebufferMs= */ 0); /* bufferForPlaybackAfterRebufferMs= */ 0);
build(); build();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED)) assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US,
SPEED))
.isFalse(); .isFalse();
assertThat( assertThat(
loadControl.shouldContinueLoading( loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, MAX_BUFFER_US - 1, SPEED)) playerId,
.isFalse(); timeline,
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MIN_BUFFER_US, SPEED)) mediaPeriodId,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US - 1,
SPEED))
.isFalse(); .isFalse();
assertThat( assertThat(
loadControl.shouldContinueLoading( loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, MIN_BUFFER_US - 1, SPEED)) playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US,
SPEED))
.isFalse();
assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US - 1,
SPEED))
.isTrue(); .isTrue();
} }
@ -96,13 +229,27 @@ public class DefaultLoadControlTest {
/* bufferForPlaybackAfterRebufferMs= */ 0); /* bufferForPlaybackAfterRebufferMs= */ 0);
build(); build();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED)) assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US,
SPEED))
.isFalse(); .isFalse();
assertThat( assertThat(
loadControl.shouldContinueLoading( loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, 5 * C.MICROS_PER_SECOND, SPEED)) playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
5 * C.MICROS_PER_SECOND,
SPEED))
.isFalse(); .isFalse();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, 500L, SPEED)) assertThat(
loadControl.shouldContinueLoading(
playerId, timeline, mediaPeriodId, /* playbackPositionUs= */ 0L, 500L, SPEED))
.isTrue(); .isTrue();
} }
@ -119,15 +266,39 @@ public class DefaultLoadControlTest {
assertThat( assertThat(
loadControl.shouldContinueLoading( loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, SPEED)) playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
/* bufferedDurationUs= */ 0L,
SPEED))
.isTrue(); .isTrue();
assertThat( assertThat(
loadControl.shouldContinueLoading( loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, MIN_BUFFER_US - 1, SPEED)) playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US - 1,
SPEED))
.isTrue(); .isTrue();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MIN_BUFFER_US, SPEED)) assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US,
SPEED))
.isFalse(); .isFalse();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED)) assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US,
SPEED))
.isFalse(); .isFalse();
} }
@ -140,21 +311,50 @@ public class DefaultLoadControlTest {
// Put loadControl in buffering state. // Put loadControl in buffering state.
assertThat( assertThat(
loadControl.shouldContinueLoading( loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, SPEED)) playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
/* bufferedDurationUs= */ 0L,
SPEED))
.isTrue(); .isTrue();
makeSureTargetBufferBytesReached(); makeSureTargetBufferBytesReached();
assertThat( assertThat(
loadControl.shouldContinueLoading( loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, SPEED)) playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
/* bufferedDurationUs= */ 0L,
SPEED))
.isFalse(); .isFalse();
assertThat( assertThat(
loadControl.shouldContinueLoading( loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, MIN_BUFFER_US - 1, SPEED)) playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US - 1,
SPEED))
.isFalse(); .isFalse();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MIN_BUFFER_US, SPEED)) assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US,
SPEED))
.isFalse(); .isFalse();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED)) assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US,
SPEED))
.isFalse(); .isFalse();
} }
@ -168,28 +368,47 @@ public class DefaultLoadControlTest {
build(); build();
// 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(/* playbackPositionUs= */ 0, MIN_BUFFER_US, SPEED)) assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US,
SPEED))
.isFalse(); .isFalse();
// At double playback speed, we continue loading. // At double playback speed, we continue loading.
assertThat( assertThat(
loadControl.shouldContinueLoading( loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, MIN_BUFFER_US, /* playbackSpeed= */ 2f)) playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US,
/* playbackSpeed= */ 2f))
.isTrue(); .isTrue();
} }
@Test @Test
public void shouldContinueLoading_withNoSelectedTracks_returnsTrue() { public void shouldContinueLoading_withNoSelectedTracks_returnsTrue() {
loadControl = builder.build(); loadControl = builder.build();
loadControl.onPrepared(playerId);
loadControl.onTracksSelected( loadControl.onTracksSelected(
Timeline.EMPTY, playerId,
LoadControl.EMPTY_MEDIA_PERIOD_ID, timeline,
mediaPeriodId,
new Renderer[0], new Renderer[0],
TrackGroupArray.EMPTY, TrackGroupArray.EMPTY,
new ExoTrackSelection[0]); new ExoTrackSelection[0]);
assertThat( assertThat(
loadControl.shouldContinueLoading( loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, /* playbackSpeed= */ 1f)) playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
/* bufferedDurationUs= */ 0L,
/* playbackSpeed= */ 1f))
.isTrue(); .isTrue();
} }
@ -199,7 +418,12 @@ public class DefaultLoadControlTest {
assertThat( assertThat(
loadControl.shouldContinueLoading( loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, MAX_BUFFER_US, /* playbackSpeed= */ 100f)) playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US,
/* playbackSpeed= */ 100f))
.isFalse(); .isFalse();
} }
@ -209,8 +433,9 @@ public class DefaultLoadControlTest {
assertThat( assertThat(
loadControl.shouldStartPlayback( loadControl.shouldStartPlayback(
Timeline.EMPTY, playerId,
LoadControl.EMPTY_MEDIA_PERIOD_ID, timeline,
mediaPeriodId,
MIN_BUFFER_US, MIN_BUFFER_US,
SPEED, SPEED,
/* rebuffering= */ false, /* rebuffering= */ false,
@ -230,18 +455,20 @@ public class DefaultLoadControlTest {
assertThat( assertThat(
loadControl.shouldStartPlayback( loadControl.shouldStartPlayback(
Timeline.EMPTY, playerId,
LoadControl.EMPTY_MEDIA_PERIOD_ID, timeline,
/* bufferedDurationUs= */ 2_999_999, mediaPeriodId,
/* bufferedDurationUs= */ 2_999_999L,
SPEED, SPEED,
/* rebuffering= */ false, /* rebuffering= */ false,
/* targetLiveOffsetUs= */ C.TIME_UNSET)) /* targetLiveOffsetUs= */ C.TIME_UNSET))
.isFalse(); .isFalse();
assertThat( assertThat(
loadControl.shouldStartPlayback( loadControl.shouldStartPlayback(
Timeline.EMPTY, playerId,
LoadControl.EMPTY_MEDIA_PERIOD_ID, timeline,
/* bufferedDurationUs= */ 3_000_000, mediaPeriodId,
/* bufferedDurationUs= */ 3_000_000L,
SPEED, SPEED,
/* rebuffering= */ false, /* rebuffering= */ false,
/* targetLiveOffsetUs= */ C.TIME_UNSET)) /* targetLiveOffsetUs= */ C.TIME_UNSET))
@ -259,21 +486,23 @@ public class DefaultLoadControlTest {
assertThat( assertThat(
loadControl.shouldStartPlayback( loadControl.shouldStartPlayback(
Timeline.EMPTY, playerId,
LoadControl.EMPTY_MEDIA_PERIOD_ID, timeline,
/* bufferedDurationUs= */ 499_999, mediaPeriodId,
/* bufferedDurationUs= */ 499_999L,
SPEED, SPEED,
/* rebuffering= */ true, /* rebuffering= */ true,
/* targetLiveOffsetUs= */ 1_000_000)) /* targetLiveOffsetUs= */ 1_000_000L))
.isFalse(); .isFalse();
assertThat( assertThat(
loadControl.shouldStartPlayback( loadControl.shouldStartPlayback(
Timeline.EMPTY, playerId,
LoadControl.EMPTY_MEDIA_PERIOD_ID, timeline,
/* bufferedDurationUs= */ 500_000, mediaPeriodId,
/* bufferedDurationUs= */ 500_000L,
SPEED, SPEED,
/* rebuffering= */ true, /* rebuffering= */ true,
/* targetLiveOffsetUs= */ 1_000_000)) /* targetLiveOffsetUs= */ 1_000_000L))
.isTrue(); .isTrue();
} }
@ -289,18 +518,20 @@ public class DefaultLoadControlTest {
assertThat( assertThat(
loadControl.shouldStartPlayback( loadControl.shouldStartPlayback(
Timeline.EMPTY, playerId,
LoadControl.EMPTY_MEDIA_PERIOD_ID, timeline,
/* bufferedDurationUs= */ 3_999_999, mediaPeriodId,
/* bufferedDurationUs= */ 3_999_999L,
SPEED, SPEED,
/* rebuffering= */ true, /* rebuffering= */ true,
/* targetLiveOffsetUs= */ C.TIME_UNSET)) /* targetLiveOffsetUs= */ C.TIME_UNSET))
.isFalse(); .isFalse();
assertThat( assertThat(
loadControl.shouldStartPlayback( loadControl.shouldStartPlayback(
Timeline.EMPTY, playerId,
LoadControl.EMPTY_MEDIA_PERIOD_ID, timeline,
/* bufferedDurationUs= */ 4_000_000, mediaPeriodId,
/* bufferedDurationUs= */ 4_000_000L,
SPEED, SPEED,
/* rebuffering= */ true, /* rebuffering= */ true,
/* targetLiveOffsetUs= */ C.TIME_UNSET)) /* targetLiveOffsetUs= */ C.TIME_UNSET))
@ -318,29 +549,106 @@ public class DefaultLoadControlTest {
assertThat( assertThat(
loadControl.shouldStartPlayback( loadControl.shouldStartPlayback(
Timeline.EMPTY, playerId,
LoadControl.EMPTY_MEDIA_PERIOD_ID, timeline,
/* bufferedDurationUs= */ 499_999, mediaPeriodId,
/* bufferedDurationUs= */ 499_999L,
SPEED, SPEED,
/* rebuffering= */ true, /* rebuffering= */ true,
/* targetLiveOffsetUs= */ 1_000_000)) /* targetLiveOffsetUs= */ 1_000_000L))
.isFalse(); .isFalse();
assertThat( assertThat(
loadControl.shouldStartPlayback( loadControl.shouldStartPlayback(
Timeline.EMPTY, playerId,
LoadControl.EMPTY_MEDIA_PERIOD_ID, timeline,
/* bufferedDurationUs= */ 500_000, mediaPeriodId,
/* bufferedDurationUs= */ 500_000L,
SPEED, SPEED,
/* rebuffering= */ true, /* rebuffering= */ true,
/* targetLiveOffsetUs= */ 1_000_000)) /* targetLiveOffsetUs= */ 1_000_000L))
.isTrue(); .isTrue();
} }
@Test
public void onPrepared_updatesTargetBufferBytes_correctDefaultTargetBufferSize() {
PlayerId playerId2 = new PlayerId(/* playerName= */ "");
loadControl = builder.setAllocator(allocator).build();
loadControl.onPrepared(playerId);
loadControl.onPrepared(playerId2);
assertThat(loadControl.calculateTotalTargetBufferBytes())
.isEqualTo(2 * DefaultLoadControl.DEFAULT_MIN_BUFFER_SIZE);
}
@Test
public void onTrackSelected_updatesTargetBufferBytes_correctTargetBufferSizeFromTrackType() {
PlayerId playerId2 = new PlayerId(/* playerName= */ "");
loadControl = builder.setAllocator(allocator).build();
loadControl.onPrepared(playerId);
loadControl.onPrepared(playerId2);
Timeline timeline2 = new FakeTimeline();
MediaSource.MediaPeriodId mediaPeriodId2 =
new MediaSource.MediaPeriodId(
timeline.getPeriod(/* periodIndex= */ 0, new Timeline.Period()));
TrackGroup videoTrackGroup =
new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build());
TrackGroupArray videoTrackGroupArray = new TrackGroupArray(videoTrackGroup);
Renderer[] videoRenderer = new Renderer[] {new FakeRenderer(C.TRACK_TYPE_VIDEO)};
TrackGroup audioTrackGroup =
new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build());
TrackGroupArray audioTrackGroupArray = new TrackGroupArray(audioTrackGroup);
Renderer[] audioRenderer = new Renderer[] {new FakeRenderer(C.TRACK_TYPE_AUDIO)};
loadControl.onTracksSelected(
playerId,
timeline,
mediaPeriodId,
videoRenderer,
videoTrackGroupArray,
new ExoTrackSelection[] {new FixedTrackSelection(videoTrackGroup, /* track= */ 0)});
loadControl.onTracksSelected(
playerId2,
timeline2,
mediaPeriodId2,
audioRenderer,
audioTrackGroupArray,
new ExoTrackSelection[] {new FixedTrackSelection(audioTrackGroup, /* track= */ 0)});
assertThat(loadControl.calculateTotalTargetBufferBytes())
.isEqualTo((2000 * C.DEFAULT_BUFFER_SEGMENT_SIZE) + (200 * C.DEFAULT_BUFFER_SEGMENT_SIZE));
}
@Test
public void onRelease_removesLoadingStateOfPlayer() {
PlayerId playerId2 = new PlayerId(/* playerName= */ "");
loadControl = builder.setAllocator(allocator).build();
loadControl.onPrepared(playerId);
loadControl.onPrepared(playerId2);
assertThat(loadControl.calculateTotalTargetBufferBytes())
.isEqualTo(2 * DefaultLoadControl.DEFAULT_MIN_BUFFER_SIZE);
loadControl.onReleased(playerId);
assertThat(loadControl.calculateTotalTargetBufferBytes())
.isEqualTo(DefaultLoadControl.DEFAULT_MIN_BUFFER_SIZE);
loadControl.onReleased(playerId2);
assertThat(loadControl.calculateTotalTargetBufferBytes()).isEqualTo(0);
}
private void build() { private void build() {
builder.setAllocator(allocator).setTargetBufferBytes(TARGET_BUFFER_BYTES); builder.setAllocator(allocator).setTargetBufferBytes(TARGET_BUFFER_BYTES);
loadControl = builder.build(); loadControl = builder.build();
loadControl.onPrepared(playerId);
loadControl.onTracksSelected( loadControl.onTracksSelected(
Timeline.EMPTY, LoadControl.EMPTY_MEDIA_PERIOD_ID, new Renderer[0], null, null); playerId,
timeline,
mediaPeriodId,
new Renderer[0],
/* trackGroups= */ null,
/* trackSelections= */ null);
} }
private void makeSureTargetBufferBytesReached() { private void makeSureTargetBufferBytesReached() {