From 08cc6e673d582037e88c2ccfd693f0e63063c7bf Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 5 Apr 2024 03:24:02 -0700 Subject: [PATCH] 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 --- .../media3/exoplayer/DefaultLoadControl.java | 109 +++-- .../exoplayer/upstream/DefaultAllocator.java | 6 +- .../exoplayer/DefaultLoadControlTest.java | 424 +++++++++++++++--- 3 files changed, 450 insertions(+), 89 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultLoadControl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultLoadControl.java index 6b385d7cbb..4861312f88 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultLoadControl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultLoadControl.java @@ -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 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; + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultAllocator.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultAllocator.java index dcc87ac48f..334c23885b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultAllocator.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultAllocator.java @@ -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 { *

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. */ diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/DefaultLoadControlTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/DefaultLoadControlTest.java index 6bd4754f75..0db4cc446e 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/DefaultLoadControlTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/DefaultLoadControlTest.java @@ -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() {