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;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static java.lang.Math.max;
import static java.lang.Math.min;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.DefaultAllocator;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.HashMap;
/** The default {@link LoadControl} implementation. */
@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
* size will be calculated based on the selected tracks.
* Sets the target buffer size in bytes for each player. The actual overall target buffer size
* 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.
* @return This builder, for convenience.
@ -262,9 +268,9 @@ public class DefaultLoadControl implements LoadControl {
private final boolean prioritizeTimeOverSizeThresholds;
private final long backBufferDurationUs;
private final boolean retainBackBufferFromKeyframe;
private final HashMap<PlayerId, PlayerLoadingState> loadingStates;
private int targetBufferBytes;
private boolean isLoading;
private long threadId;
/** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */
public DefaultLoadControl() {
@ -308,42 +314,53 @@ public class DefaultLoadControl implements LoadControl {
this.bufferForPlaybackUs = Util.msToUs(bufferForPlaybackMs);
this.bufferForPlaybackAfterRebufferUs = Util.msToUs(bufferForPlaybackAfterRebufferMs);
this.targetBufferBytesOverwrite = targetBufferBytes;
this.targetBufferBytes =
targetBufferBytesOverwrite != C.LENGTH_UNSET
? targetBufferBytesOverwrite
: DEFAULT_MIN_BUFFER_SIZE;
this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;
this.backBufferDurationUs = Util.msToUs(backBufferDurationMs);
this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe;
loadingStates = new HashMap<>();
threadId = C.INDEX_UNSET;
}
@Override
public void onPrepared() {
reset(false);
public void onPrepared(PlayerId playerId) {
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
public void onTracksSelected(
PlayerId playerId,
Timeline timeline,
MediaPeriodId mediaPeriodId,
Renderer[] renderers,
TrackGroupArray trackGroups,
ExoTrackSelection[] trackSelections) {
targetBufferBytes =
checkNotNull(loadingStates.get(playerId)).targetBufferBytes =
targetBufferBytesOverwrite == C.LENGTH_UNSET
? calculateTargetBufferBytes(renderers, trackSelections)
: targetBufferBytesOverwrite;
allocator.setTargetBufferSize(targetBufferBytes);
updateAllocator();
}
@Override
public void onStopped() {
reset(true);
public void onStopped(PlayerId playerId) {
removePlayer(playerId);
}
@Override
public void onReleased() {
reset(true);
public void onReleased(PlayerId playerId) {
removePlayer(playerId);
if (loadingStates.isEmpty()) {
threadId = C.INDEX_UNSET;
}
}
@Override
@ -352,19 +369,26 @@ public class DefaultLoadControl implements LoadControl {
}
@Override
public long getBackBufferDurationUs() {
public long getBackBufferDurationUs(PlayerId playerId) {
return backBufferDurationUs;
}
@Override
public boolean retainBackBufferFromKeyframe() {
public boolean retainBackBufferFromKeyframe(PlayerId playerId) {
return retainBackBufferFromKeyframe;
}
@Override
public boolean shouldContinueLoading(
long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) {
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferBytes;
PlayerId playerId,
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;
if (playbackSpeed > 1) {
// 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.
minBufferUs = max(minBufferUs, 500_000);
if (bufferedDurationUs < minBufferUs) {
isLoading = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached;
if (!isLoading && bufferedDurationUs < 500_000) {
playerLoadingState.isLoading = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached;
if (!playerLoadingState.isLoading && bufferedDurationUs < 500_000) {
Log.w(
"DefaultLoadControl",
"Target buffer size reached with less than 500ms of buffered media data.");
}
} else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) {
isLoading = false;
playerLoadingState.isLoading = false;
} // Else don't change the loading state.
return isLoading;
return playerLoadingState.isLoading;
}
@Override
public boolean shouldStartPlayback(
PlayerId playerId,
Timeline timeline,
MediaPeriodId mediaPeriodId,
long bufferedDurationUs,
@ -404,7 +429,7 @@ public class DefaultLoadControl implements LoadControl {
return minBufferDurationUs <= 0
|| bufferedDurationUs >= minBufferDurationUs
|| (!prioritizeTimeOverSizeThresholds
&& allocator.getTotalBytesAllocated() >= targetBufferBytes);
&& allocator.getTotalBytesAllocated() >= calculateTotalTargetBufferBytes());
}
/**
@ -426,14 +451,35 @@ public class DefaultLoadControl implements LoadControl {
return max(DEFAULT_MIN_BUFFER_SIZE, targetBufferSize);
}
private void reset(boolean resetAllocator) {
targetBufferBytes =
@VisibleForTesting
/* 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
? DEFAULT_MIN_BUFFER_SIZE
: targetBufferBytesOverwrite;
isLoading = false;
if (resetAllocator) {
playerLoadingState.isLoading = false;
}
private void removePlayer(PlayerId playerId) {
if (loadingStates.remove(playerId) != null) {
updateAllocator();
}
}
private void updateAllocator() {
if (loadingStates.isEmpty()) {
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) {
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.
*
* @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}.
*/
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()}.
*
* @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 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 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.TrackGroup;
import androidx.media3.common.util.Util;
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.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.trackselection.FixedTrackSelection;
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 org.junit.Before;
import org.junit.Test;
@ -41,27 +51,126 @@ public class DefaultLoadControlTest {
private Builder builder;
private DefaultAllocator allocator;
private DefaultLoadControl loadControl;
private PlayerId playerId;
private Timeline timeline;
private MediaSource.MediaPeriodId mediaPeriodId;
@Before
public void setUp() throws Exception {
builder = new Builder();
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
public void shouldContinueLoading_untilMaxBufferExceeded() {
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(
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();
assertThat(
loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, MAX_BUFFER_US - 1, SPEED))
.isTrue();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED))
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US,
SPEED))
.isFalse();
assertThat(
loadControl.shouldContinueLoading(
playerId2,
timeline2,
mediaPeriodId2,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US - 1,
SPEED))
.isTrue();
}
@Test
@ -73,17 +182,41 @@ public class DefaultLoadControlTest {
/* bufferForPlaybackAfterRebufferMs= */ 0);
build();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED))
assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US,
SPEED))
.isFalse();
assertThat(
loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, MAX_BUFFER_US - 1, SPEED))
.isFalse();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MIN_BUFFER_US, SPEED))
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US - 1,
SPEED))
.isFalse();
assertThat(
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();
}
@ -96,13 +229,27 @@ public class DefaultLoadControlTest {
/* bufferForPlaybackAfterRebufferMs= */ 0);
build();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED))
assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US,
SPEED))
.isFalse();
assertThat(
loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, 5 * C.MICROS_PER_SECOND, SPEED))
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
5 * C.MICROS_PER_SECOND,
SPEED))
.isFalse();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, 500L, SPEED))
assertThat(
loadControl.shouldContinueLoading(
playerId, timeline, mediaPeriodId, /* playbackPositionUs= */ 0L, 500L, SPEED))
.isTrue();
}
@ -119,15 +266,39 @@ public class DefaultLoadControlTest {
assertThat(
loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, SPEED))
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
/* bufferedDurationUs= */ 0L,
SPEED))
.isTrue();
assertThat(
loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, MIN_BUFFER_US - 1, SPEED))
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US - 1,
SPEED))
.isTrue();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MIN_BUFFER_US, SPEED))
assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US,
SPEED))
.isFalse();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED))
assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US,
SPEED))
.isFalse();
}
@ -140,21 +311,50 @@ public class DefaultLoadControlTest {
// Put loadControl in buffering state.
assertThat(
loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, SPEED))
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
/* bufferedDurationUs= */ 0L,
SPEED))
.isTrue();
makeSureTargetBufferBytesReached();
assertThat(
loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, SPEED))
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
/* bufferedDurationUs= */ 0L,
SPEED))
.isFalse();
assertThat(
loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, MIN_BUFFER_US - 1, SPEED))
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US - 1,
SPEED))
.isFalse();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MIN_BUFFER_US, SPEED))
assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US,
SPEED))
.isFalse();
assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED))
assertThat(
loadControl.shouldContinueLoading(
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US,
SPEED))
.isFalse();
}
@ -168,28 +368,47 @@ public class DefaultLoadControlTest {
build();
// 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();
// At double playback speed, we continue loading.
assertThat(
loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, MIN_BUFFER_US, /* playbackSpeed= */ 2f))
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MIN_BUFFER_US,
/* playbackSpeed= */ 2f))
.isTrue();
}
@Test
public void shouldContinueLoading_withNoSelectedTracks_returnsTrue() {
loadControl = builder.build();
loadControl.onPrepared(playerId);
loadControl.onTracksSelected(
Timeline.EMPTY,
LoadControl.EMPTY_MEDIA_PERIOD_ID,
playerId,
timeline,
mediaPeriodId,
new Renderer[0],
TrackGroupArray.EMPTY,
new ExoTrackSelection[0]);
assertThat(
loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, /* playbackSpeed= */ 1f))
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
/* bufferedDurationUs= */ 0L,
/* playbackSpeed= */ 1f))
.isTrue();
}
@ -199,7 +418,12 @@ public class DefaultLoadControlTest {
assertThat(
loadControl.shouldContinueLoading(
/* playbackPositionUs= */ 0, MAX_BUFFER_US, /* playbackSpeed= */ 100f))
playerId,
timeline,
mediaPeriodId,
/* playbackPositionUs= */ 0L,
MAX_BUFFER_US,
/* playbackSpeed= */ 100f))
.isFalse();
}
@ -209,8 +433,9 @@ public class DefaultLoadControlTest {
assertThat(
loadControl.shouldStartPlayback(
Timeline.EMPTY,
LoadControl.EMPTY_MEDIA_PERIOD_ID,
playerId,
timeline,
mediaPeriodId,
MIN_BUFFER_US,
SPEED,
/* rebuffering= */ false,
@ -230,18 +455,20 @@ public class DefaultLoadControlTest {
assertThat(
loadControl.shouldStartPlayback(
Timeline.EMPTY,
LoadControl.EMPTY_MEDIA_PERIOD_ID,
/* bufferedDurationUs= */ 2_999_999,
playerId,
timeline,
mediaPeriodId,
/* bufferedDurationUs= */ 2_999_999L,
SPEED,
/* rebuffering= */ false,
/* targetLiveOffsetUs= */ C.TIME_UNSET))
.isFalse();
assertThat(
loadControl.shouldStartPlayback(
Timeline.EMPTY,
LoadControl.EMPTY_MEDIA_PERIOD_ID,
/* bufferedDurationUs= */ 3_000_000,
playerId,
timeline,
mediaPeriodId,
/* bufferedDurationUs= */ 3_000_000L,
SPEED,
/* rebuffering= */ false,
/* targetLiveOffsetUs= */ C.TIME_UNSET))
@ -259,21 +486,23 @@ public class DefaultLoadControlTest {
assertThat(
loadControl.shouldStartPlayback(
Timeline.EMPTY,
LoadControl.EMPTY_MEDIA_PERIOD_ID,
/* bufferedDurationUs= */ 499_999,
playerId,
timeline,
mediaPeriodId,
/* bufferedDurationUs= */ 499_999L,
SPEED,
/* rebuffering= */ true,
/* targetLiveOffsetUs= */ 1_000_000))
/* targetLiveOffsetUs= */ 1_000_000L))
.isFalse();
assertThat(
loadControl.shouldStartPlayback(
Timeline.EMPTY,
LoadControl.EMPTY_MEDIA_PERIOD_ID,
/* bufferedDurationUs= */ 500_000,
playerId,
timeline,
mediaPeriodId,
/* bufferedDurationUs= */ 500_000L,
SPEED,
/* rebuffering= */ true,
/* targetLiveOffsetUs= */ 1_000_000))
/* targetLiveOffsetUs= */ 1_000_000L))
.isTrue();
}
@ -289,18 +518,20 @@ public class DefaultLoadControlTest {
assertThat(
loadControl.shouldStartPlayback(
Timeline.EMPTY,
LoadControl.EMPTY_MEDIA_PERIOD_ID,
/* bufferedDurationUs= */ 3_999_999,
playerId,
timeline,
mediaPeriodId,
/* bufferedDurationUs= */ 3_999_999L,
SPEED,
/* rebuffering= */ true,
/* targetLiveOffsetUs= */ C.TIME_UNSET))
.isFalse();
assertThat(
loadControl.shouldStartPlayback(
Timeline.EMPTY,
LoadControl.EMPTY_MEDIA_PERIOD_ID,
/* bufferedDurationUs= */ 4_000_000,
playerId,
timeline,
mediaPeriodId,
/* bufferedDurationUs= */ 4_000_000L,
SPEED,
/* rebuffering= */ true,
/* targetLiveOffsetUs= */ C.TIME_UNSET))
@ -318,29 +549,106 @@ public class DefaultLoadControlTest {
assertThat(
loadControl.shouldStartPlayback(
Timeline.EMPTY,
LoadControl.EMPTY_MEDIA_PERIOD_ID,
/* bufferedDurationUs= */ 499_999,
playerId,
timeline,
mediaPeriodId,
/* bufferedDurationUs= */ 499_999L,
SPEED,
/* rebuffering= */ true,
/* targetLiveOffsetUs= */ 1_000_000))
/* targetLiveOffsetUs= */ 1_000_000L))
.isFalse();
assertThat(
loadControl.shouldStartPlayback(
Timeline.EMPTY,
LoadControl.EMPTY_MEDIA_PERIOD_ID,
/* bufferedDurationUs= */ 500_000,
playerId,
timeline,
mediaPeriodId,
/* bufferedDurationUs= */ 500_000L,
SPEED,
/* rebuffering= */ true,
/* targetLiveOffsetUs= */ 1_000_000))
/* targetLiveOffsetUs= */ 1_000_000L))
.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() {
builder.setAllocator(allocator).setTargetBufferBytes(TARGET_BUFFER_BYTES);
loadControl = builder.build();
loadControl.onPrepared(playerId);
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() {