Preload first period of next window

Allow apps to preload the first period of the next window in
the playlist of `ExoPlayer`. By default playlist preloading is
disabled. To enable preloading,
`ExoPlayer.setPreloadConfiguration(PreloadConfiguration)` can be
called.

`LoadControl` determines when to preload with its implemenation of `shouldContinuePreloading(timeline, mediaPeriodId, bufferedDurationUs)`.
The implementation in `DefaultLoadControl` allows preloading only when
the player isn't currently loading for playback. Apps can override this
behaviour.

Issue: androidx/media#468
PiperOrigin-RevId: 677786017
This commit is contained in:
bachinger 2024-09-23 07:30:22 -07:00 committed by Copybara-Service
parent 3d3ec85c12
commit ba1cdba403
8 changed files with 403 additions and 75 deletions

View File

@ -34,6 +34,17 @@
* Add `ForwardingRenderer` implementation that forwards all method calls
to another renderer
([1703](https://github.com/androidx/media/pull/1703)).
* Add playlist preloading for the next item in the playlist. Apps can
enable preloading by calling
`ExoPlayer.setPreloadConfiguration(PreloadConfiguration)` accordingly.
By default preloading is disabled. When opted-in and to not interfer
with playback, `DefaultLoadControl` restricts preloading to start and
continue only when the player is not loading for playback. Apps can
change this behaviour by implementing
`LoadControl.shouldContinuePreloading()` accordingly (like when
overriding this method in `DefaultLoadControl`). The default
implementation of `LoadControl` disables preloading in case an app is
using a custom implementation of `LoadControl`.
* Transformer:
* Track Selection:
* Extractors:

View File

@ -422,6 +422,17 @@ public class DefaultLoadControl implements LoadControl {
&& allocator.getTotalBytesAllocated() >= calculateTotalTargetBufferBytes());
}
@Override
public boolean shouldContinuePreloading(
Timeline timeline, MediaPeriodId mediaPeriodId, long bufferedDurationUs) {
for (PlayerLoadingState playerLoadingState : loadingStates.values()) {
if (playerLoadingState.isLoading) {
return false;
}
}
return true;
}
/**
* Calculate target buffer size in bytes based on the selected tracks. The player will try not to
* exceed this target buffer. Only used when {@code targetBufferBytes} is {@link C#LENGTH_UNSET}.

View File

@ -16,6 +16,7 @@
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.Util.castNonNull;
import static androidx.media3.common.util.Util.msToUs;
import static androidx.media3.exoplayer.Renderer.STATE_DISABLED;
@ -340,7 +341,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
loadControl.getAllocator(),
mediaSourceList,
mediaPeriodInfo,
emptyTrackSelectorResult);
emptyTrackSelectorResult,
preloadConfiguration.targetPreloadDurationUs);
}
public void experimentalSetForegroundModeTimeoutMs(long setForegroundModeTimeoutMs) {
@ -1213,7 +1215,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
if (!playbackInfo.isLoading
&& playbackInfo.totalBufferedDurationUs < PLAYBACK_BUFFER_EMPTY_THRESHOLD_US
&& isLoadingPossible()) {
&& isLoadingPossible(queue.getLoadingPeriod())) {
// The renderers are not ready, there is more media available to load, and the LoadControl
// is refusing to load it (indicated by !playbackInfo.isLoading). This could be because the
// renderers are still transitioning to their ready states, but it could also indicate a
@ -2220,7 +2222,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo);
if (info != null) {
MediaPeriodHolder mediaPeriodHolder = queue.enqueueNextMediaPeriodHolder(info);
mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
if (!mediaPeriodHolder.prepareCalled) {
mediaPeriodHolder.prepare(this, info.startPositionUs);
} else if (mediaPeriodHolder.prepared) {
handler.obtainMessage(MSG_PERIOD_PREPARED, mediaPeriodHolder.mediaPeriod).sendToTarget();
}
if (queue.getPlayingPeriod() == mediaPeriodHolder) {
resetRendererPosition(info.startPositionUs);
}
@ -2231,7 +2237,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (shouldContinueLoading) {
// We should still be loading, except when there is nothing to load or we have fully loaded
// the current period.
shouldContinueLoading = isLoadingPossible();
shouldContinueLoading = isLoadingPossible(queue.getLoadingPeriod());
updateIsLoading();
} else {
maybeContinueLoading();
@ -2342,14 +2348,37 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
private void maybeUpdatePreloadPeriods(boolean loadingPeriodChanged) {
if (preloadConfiguration.targetPreloadDurationUs == C.TIME_UNSET
|| (!loadingPeriodChanged
&& playbackInfo.timeline.equals(lastPreloadPoolInvalidationTimeline))) {
// Do nothing if preloading disabled or no change in loading period or timeline has occurred.
if (preloadConfiguration.targetPreloadDurationUs == C.TIME_UNSET) {
// Do nothing if preloading disabled.
return;
}
lastPreloadPoolInvalidationTimeline = playbackInfo.timeline;
queue.invalidatePreloadPool(playbackInfo.timeline);
if (loadingPeriodChanged
|| !playbackInfo.timeline.equals(lastPreloadPoolInvalidationTimeline)) {
// invalidate the pool when the loading period or the timeline changed.
lastPreloadPoolInvalidationTimeline = playbackInfo.timeline;
queue.invalidatePreloadPool(playbackInfo.timeline);
}
maybeContinuePreloading();
}
private void maybeContinuePreloading() {
queue.maybeUpdatePreloadMediaPeriodHolder();
MediaPeriodHolder preloading = queue.getPreloadingPeriod();
if (preloading == null
|| (preloading.prepareCalled && !preloading.prepared)
|| preloading.mediaPeriod.isLoading()
|| !loadControl.shouldContinuePreloading(
playbackInfo.timeline,
preloading.info.id,
preloading.prepared ? preloading.mediaPeriod.getBufferedPositionUs() : 0L)) {
return;
}
if (!preloading.prepareCalled) {
preloading.prepare(/* callback= */ this, preloading.info.startPositionUs);
} else {
preloading.continueLoading(
rendererPositionUs, playbackInfo.playbackParameters.speed, lastRebufferRealtimeMs);
}
}
private boolean replaceStreamsOrDisableRendererForTransition() throws ExoPlaybackException {
@ -2531,13 +2560,27 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException {
if (!queue.isLoading(mediaPeriod)) {
// Stale event.
return;
if (queue.isLoading(mediaPeriod)) {
handleLoadingPeriodPrepared(checkNotNull(queue.getLoadingPeriod()));
} else {
@Nullable MediaPeriodHolder preloadHolder = queue.getPreloadHolderByMediaPeriod(mediaPeriod);
if (preloadHolder != null) {
checkState(!preloadHolder.prepared);
preloadHolder.handlePrepared(
mediaClock.getPlaybackParameters().speed, playbackInfo.timeline);
if (queue.isPreloading(mediaPeriod)) {
maybeContinuePreloading();
}
}
}
}
private void handleLoadingPeriodPrepared(MediaPeriodHolder loadingPeriodHolder)
throws ExoPlaybackException {
if (!loadingPeriodHolder.prepared) {
loadingPeriodHolder.handlePrepared(
mediaClock.getPlaybackParameters().speed, playbackInfo.timeline);
}
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
loadingPeriodHolder.handlePrepared(
mediaClock.getPlaybackParameters().speed, playbackInfo.timeline);
updateLoadControlTrackSelection(
loadingPeriodHolder.info.id,
loadingPeriodHolder.getTrackGroups(),
@ -2559,12 +2602,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
private void handleContinueLoadingRequested(MediaPeriod mediaPeriod) {
if (!queue.isLoading(mediaPeriod)) {
// Stale event.
return;
if (queue.isLoading(mediaPeriod)) {
queue.reevaluateBuffer(rendererPositionUs);
maybeContinueLoading();
} else if (queue.isPreloading(mediaPeriod)) {
maybeContinuePreloading();
}
queue.reevaluateBuffer(rendererPositionUs);
maybeContinueLoading();
}
private void handlePlaybackParameters(
@ -2610,7 +2653,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
private boolean shouldContinueLoading() {
if (!isLoadingPossible()) {
if (!isLoadingPossible(queue.getLoadingPeriod())) {
return false;
}
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
@ -2651,19 +2694,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
return shouldContinueLoading;
}
private boolean isLoadingPossible() {
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
if (loadingPeriodHolder == null) {
return false;
}
if (loadingPeriodHolder.hasLoadingError()) {
return false;
}
long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs();
if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
return false;
}
return true;
private boolean isLoadingPossible(@Nullable MediaPeriodHolder mediaPeriodHolder) {
return mediaPeriodHolder != null
&& !mediaPeriodHolder.hasLoadingError()
&& mediaPeriodHolder.getNextLoadPositionUs() != C.TIME_END_OF_SOURCE;
}
private void updateIsLoading() {

View File

@ -19,6 +19,7 @@ import androidx.media3.common.C;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.MediaPeriod;
@ -324,6 +325,24 @@ public interface LoadControl {
throw new IllegalStateException("shouldContinueLoading not implemented");
}
/**
* Called to determine whether preloading should be continued. If this method returns true, the
* presented period will continue to load media.
*
* @param timeline The Timeline containing the preload period that can be looked up with
* MediaPeriodId.periodUid.
* @param mediaPeriodId The MediaPeriodId of the preloading period.
* @param bufferedDurationUs The duration of media currently buffered by the preload period.
* @return Whether the preloading should continue for the given period.
*/
default boolean shouldContinuePreloading(
Timeline timeline, MediaPeriodId mediaPeriodId, long bufferedDurationUs) {
Log.w(
"LoadControl",
"shouldContinuePreloading needs to be implemented when playlist preloading is enabled");
return false;
}
/**
* Called repeatedly by the player when it's loading the source, has yet to start playback, and
* has the minimum amount of data necessary for playback to be started. The value returned

View File

@ -53,6 +53,12 @@ import java.io.IOException;
*/
public final @NullableType SampleStream[] sampleStreams;
/** The target buffer duration to preload. */
public final long targetPreloadBufferDurationUs;
/** Whether {@link #prepare(MediaPeriod.Callback, long)} has been called. */
public boolean prepareCalled;
/** Whether the media period has finished preparing. */
public boolean prepared;
@ -103,13 +109,15 @@ import java.io.IOException;
Allocator allocator,
MediaSourceList mediaSourceList,
MediaPeriodInfo info,
TrackSelectorResult emptyTrackSelectorResult) {
TrackSelectorResult emptyTrackSelectorResult,
long targetPreloadBufferDurationUs) {
this.rendererCapabilities = rendererCapabilities;
this.rendererPositionOffsetUs = rendererPositionOffsetUs;
this.trackSelector = trackSelector;
this.mediaSourceList = mediaSourceList;
this.uid = info.id.periodUid;
this.info = info;
this.targetPreloadBufferDurationUs = targetPreloadBufferDurationUs;
this.trackGroups = TrackGroupArray.EMPTY;
this.trackSelectorResult = emptyTrackSelectorResult;
sampleStreams = new SampleStream[rendererCapabilities.length];
@ -160,6 +168,13 @@ import java.io.IOException;
&& (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE);
}
/** Returns whether the period is fully preloaded. */
public boolean isFullyPreloaded() {
return prepared
&& (isFullyBuffered()
|| getBufferedPositionUs() - info.startPositionUs >= targetPreloadBufferDurationUs);
}
/**
* Returns the buffered position in microseconds. If the period is buffered to the end, then the
* period duration is returned.
@ -509,6 +524,11 @@ import java.io.IOException;
&& this.info.id.equals(info.id);
}
public void prepare(MediaPeriod.Callback callback, long startPositionUs) {
prepareCalled = true;
mediaPeriod.prepare(callback, startPositionUs);
}
/* package */ interface Factory {
MediaPeriodHolder create(MediaPeriodInfo info, long rendererPositionOffsetUs);
}

View File

@ -79,13 +79,14 @@ import java.util.List;
private long nextWindowSequenceNumber;
private @RepeatMode int repeatMode;
private boolean shuffleModeEnabled;
private PreloadConfiguration preloadConfiguration;
@Nullable private MediaPeriodHolder playing;
@Nullable private MediaPeriodHolder reading;
@Nullable private MediaPeriodHolder loading;
@Nullable private MediaPeriodHolder preloading;
private int length;
@Nullable private Object oldFrontPeriodUid;
private long oldFrontPeriodWindowSequenceNumber;
private PreloadConfiguration preloadConfiguration;
private List<MediaPeriodHolder> preloadPriorityList;
/**
@ -153,6 +154,11 @@ import java.util.List;
return loading != null && loading.mediaPeriod == mediaPeriod;
}
/** Returns whether {@code mediaPeriod} is the current preloading media period. */
public boolean isPreloading(MediaPeriod mediaPeriod) {
return preloading != null && preloading.mediaPeriod == mediaPeriod;
}
/**
* If there is a loading period, reevaluates its buffer.
*
@ -285,6 +291,8 @@ import java.util.List;
preloadPriorityList.get(i).release();
}
preloadPriorityList = newPriorityList;
preloading = null;
maybeUpdatePreloadMediaPeriodHolder();
}
private MediaPeriodInfo getMediaPeriodInfoForPeriodPosition(
@ -333,6 +341,12 @@ import java.util.List;
return loading;
}
/** Returns the preloading period holder, or null if there is no preloading period. */
@Nullable
public MediaPeriodHolder getPreloadingPeriod() {
return preloading;
}
/**
* Returns the playing period holder which is at the front of the queue, or null if the queue is
* empty.
@ -414,6 +428,35 @@ import java.util.List;
return removedReading;
}
/**
* Sets the preloading period to the next period in the queue to preload or to null, if all
* periods in the preload pool are fully loaded.
*/
public void maybeUpdatePreloadMediaPeriodHolder() {
if (preloading != null && !preloading.isFullyPreloaded()) {
return;
}
preloading = null;
for (int i = 0; i < preloadPriorityList.size(); i++) {
MediaPeriodHolder mediaPeriodHolder = preloadPriorityList.get(i);
if (!mediaPeriodHolder.isFullyPreloaded()) {
preloading = mediaPeriodHolder;
break;
}
}
}
@Nullable
public MediaPeriodHolder getPreloadHolderByMediaPeriod(MediaPeriod mediaPeriod) {
for (int i = 0; i < preloadPriorityList.size(); i++) {
MediaPeriodHolder mediaPeriodHolder = preloadPriorityList.get(i);
if (mediaPeriodHolder.mediaPeriod == mediaPeriod) {
return mediaPeriodHolder;
}
}
return null;
}
/** Clears the queue. */
public void clear() {
if (length == 0) {
@ -734,6 +777,7 @@ import java.util.List;
for (int i = 0; i < preloadPriorityList.size(); i++) {
MediaPeriodHolder preloadHolder = preloadPriorityList.get(i);
if (preloadHolder.uid.equals(periodUid)) {
// Found a match in the preload periods.
return preloadHolder.info.id.windowSequenceNumber;
}
}

View File

@ -7196,42 +7196,6 @@ public class ExoPlayerTest {
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */);
}
@Test
public void prepare_preloadingEnabled_nextWindowPeriodCreatedForPreloading() throws Exception {
FakeMediaSource mediaSource1 =
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * 2)));
List<MediaPeriodId> createdMediaPeriodIds = new ArrayList<>();
FakeMediaSource mediaSource2 =
new FakeMediaSource() {
@Override
public MediaPeriod createPeriod(
MediaPeriodId id, Allocator allocator, long startPositionUs) {
createdMediaPeriodIds.add(id);
return super.createPeriod(id, allocator, startPositionUs);
}
};
ExoPlayer player =
// Intentionally not using `parameterizeTestExoPlayerBuilder()` for preload specific test.
new TestExoPlayerBuilder(context)
.setPreloadConfiguration(
new ExoPlayer.PreloadConfiguration(/* targetPreloadDurationUs= */ 5_000_000L))
.build();
player.setMediaSources(ImmutableList.of(mediaSource1, mediaSource2));
player.prepare();
run(player).untilPendingCommandsAreFullyHandled();
assertThat(createdMediaPeriodIds).hasSize(1);
play(player).untilState(Player.STATE_ENDED);
assertThat(createdMediaPeriodIds).hasSize(1);
player.release();
}
@Test
public void prepare_preloadingEnabledRepeatModeOne_sameWindowPeriodCreatedForPreloading()
throws Exception {
@ -7243,7 +7207,8 @@ public class ExoPlayerTest {
/* durationUs= */ DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * 2));
List<MediaPeriodId> createdMediaPeriodIds = new ArrayList<>();
FakeMediaSource mediaSource =
new FakeMediaSource(timeline) {
new FakeMediaSource(
timeline, ExoPlayerTestRunner.AUDIO_FORMAT, ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
public MediaPeriod createPeriod(
MediaPeriodId id, Allocator allocator, long startPositionUs) {
@ -7270,6 +7235,229 @@ public class ExoPlayerTest {
player.release();
}
@Test
public void prepare_preloadingEnabled_nextWindowPeriodPreloaded() throws Exception {
List<MediaPeriodId> createdMediaPeriodIds = new ArrayList<>();
FakeMediaSource mediaSource1 =
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * 2)),
ExoPlayerTestRunner.AUDIO_FORMAT,
ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
createdMediaPeriodIds.add(id);
return super.createMediaPeriod(
id,
trackGroupArray,
allocator,
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
transferListener);
}
};
List<Long> preloadPreparationPositionUs = new ArrayList<>();
List<LoadingInfo> preloadLoadingInfos = new ArrayList<>();
FakeMediaSource mediaSource2 =
new FakeMediaSource(
new FakeTimeline(),
ExoPlayerTestRunner.AUDIO_FORMAT,
ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
createdMediaPeriodIds.add(id);
long positionInWindowUs =
getTimeline()
.getPeriodByUid(id.periodUid, new Timeline.Period())
.getPositionInWindowUs();
long defaultFirstSampleTimeUs = positionInWindowUs >= 0 ? 0 : -positionInWindowUs;
return new FakeMediaPeriod(
trackGroupArray,
allocator,
FakeMediaPeriod.TrackDataFactory.singleSampleWithTimeUs(defaultFirstSampleTimeUs),
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
/* deferOnPrepared= */ false) {
@Override
public synchronized void prepare(Callback callback, long positionUs) {
preloadPreparationPositionUs.add(positionUs);
super.prepare(callback, positionUs);
}
@Override
public boolean continueLoading(LoadingInfo loadingInfo) {
preloadLoadingInfos.add(loadingInfo);
return super.continueLoading(loadingInfo);
}
};
}
};
MediaPeriodId firstMediaPeriodId =
new MediaPeriodId(/* periodUid= */ new Pair<>(0, 0), /* windowSequenceNumber= */ 0);
MediaPeriodId secondMediaPeriodId =
new MediaPeriodId(/* periodUid= */ new Pair<>(0, 0), /* windowSequenceNumber= */ 1);
ExoPlayer player =
// Intentionally not using `parameterizeTestExoPlayerBuilder()` for preload specific test.
new TestExoPlayerBuilder(context)
.setLoadControl(
new DefaultLoadControl() {
@Override
public boolean shouldContinuePreloading(
Timeline timeline, MediaPeriodId mediaPeriodId, long bufferedDurationUs) {
return true;
}
})
.setPreloadConfiguration(
new ExoPlayer.PreloadConfiguration(/* targetPreloadDurationUs= */ 5_000_000L))
.build();
player.setMediaSources(ImmutableList.of(mediaSource1, mediaSource2));
player.prepare();
run(player).untilPendingCommandsAreFullyHandled();
// Assert both media periods are created, prepared and loaded when paused after preparation.
assertThat(createdMediaPeriodIds)
.containsExactly(firstMediaPeriodId, secondMediaPeriodId)
.inOrder();
assertThat(preloadPreparationPositionUs).containsExactly(123_000_000L);
assertThat(preloadLoadingInfos).hasSize(1);
play(player).untilState(Player.STATE_ENDED);
assertThat(createdMediaPeriodIds)
.containsExactly(firstMediaPeriodId, secondMediaPeriodId)
.inOrder();
// Verify that the preloaded period from the pool was used for enqueuing.
assertThat(preloadPreparationPositionUs).containsExactly(123_000_000L);
assertThat(preloadLoadingInfos).hasSize(1);
player.release();
}
@Test
public void prepare_preloadingDisabled_nextWindowPeriodNotPreloaded() throws Exception {
List<MediaPeriodId> createdMediaPeriodIds = new ArrayList<>();
FakeMediaSource mediaSource1 =
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * 2)),
ExoPlayerTestRunner.AUDIO_FORMAT,
ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
createdMediaPeriodIds.add(id);
return super.createMediaPeriod(
id,
trackGroupArray,
allocator,
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
transferListener);
}
};
List<Long> preloadPreparationPositionUs = new ArrayList<>();
List<LoadingInfo> preloadLoadingInfos = new ArrayList<>();
FakeMediaSource mediaSource2 =
new FakeMediaSource(
new FakeTimeline(),
ExoPlayerTestRunner.AUDIO_FORMAT,
ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
createdMediaPeriodIds.add(id);
long positionInWindowUs =
getTimeline()
.getPeriodByUid(id.periodUid, new Timeline.Period())
.getPositionInWindowUs();
long defaultFirstSampleTimeUs = positionInWindowUs >= 0 ? 0 : -positionInWindowUs;
return new FakeMediaPeriod(
trackGroupArray,
allocator,
FakeMediaPeriod.TrackDataFactory.singleSampleWithTimeUs(defaultFirstSampleTimeUs),
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
/* deferOnPrepared= */ false) {
@Override
public synchronized void prepare(Callback callback, long positionUs) {
preloadPreparationPositionUs.add(positionUs);
super.prepare(callback, positionUs);
}
@Override
public boolean continueLoading(LoadingInfo loadingInfo) {
preloadLoadingInfos.add(loadingInfo);
return super.continueLoading(loadingInfo);
}
};
}
};
MediaPeriodId firstMediaPeriodId =
new MediaPeriodId(/* periodUid= */ new Pair<>(0, 0), /* windowSequenceNumber= */ 0);
MediaPeriodId secondMediaPeriodId =
new MediaPeriodId(/* periodUid= */ new Pair<>(0, 0), /* windowSequenceNumber= */ 1);
ExoPlayer player =
// Intentionally not using `parameterizeTestExoPlayerBuilder()` for preload specific test.
new TestExoPlayerBuilder(context)
.setPreloadConfiguration(ExoPlayer.PreloadConfiguration.DEFAULT)
.build();
player.setMediaSources(ImmutableList.of(mediaSource1, mediaSource2));
player.prepare();
run(player).untilPendingCommandsAreFullyHandled();
// Assert the media period of the second source isn't created yet.
assertThat(createdMediaPeriodIds).containsExactly(firstMediaPeriodId);
assertThat(preloadPreparationPositionUs).isEmpty();
assertThat(preloadLoadingInfos).isEmpty();
play(player).untilState(Player.STATE_ENDED);
// Assert the second second period is created for playback only.
assertThat(createdMediaPeriodIds)
.containsExactly(firstMediaPeriodId, secondMediaPeriodId)
.inOrder();
assertThat(preloadPreparationPositionUs).containsExactly(123_000_000L);
assertThat(preloadLoadingInfos).hasSize(1);
player.release();
}
@Test
public void seekToIndexLargerThanNumberOfPlaylistItems() throws Exception {
Timeline fakeTimeline =

View File

@ -132,7 +132,8 @@ public final class MediaPeriodQueueTest {
new RendererConfiguration[0],
new ExoTrackSelection[0],
Tracks.EMPTY,
/* info= */ null));
/* info= */ null),
/* targetPreloadBufferDurationUs= */ 5_000_000L);
},
PreloadConfiguration.DEFAULT);
mediaSourceList =