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 * Add `ForwardingRenderer` implementation that forwards all method calls
to another renderer to another renderer
([1703](https://github.com/androidx/media/pull/1703)). ([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: * Transformer:
* Track Selection: * Track Selection:
* Extractors: * Extractors:

View File

@ -422,6 +422,17 @@ public class DefaultLoadControl implements LoadControl {
&& allocator.getTotalBytesAllocated() >= calculateTotalTargetBufferBytes()); && 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 * 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}. * exceed this target buffer. Only used when {@code targetBufferBytes} is {@link C#LENGTH_UNSET}.

View File

@ -16,6 +16,7 @@
package androidx.media3.exoplayer; package androidx.media3.exoplayer;
import static androidx.media3.common.util.Assertions.checkNotNull; 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.castNonNull;
import static androidx.media3.common.util.Util.msToUs; import static androidx.media3.common.util.Util.msToUs;
import static androidx.media3.exoplayer.Renderer.STATE_DISABLED; import static androidx.media3.exoplayer.Renderer.STATE_DISABLED;
@ -340,7 +341,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
loadControl.getAllocator(), loadControl.getAllocator(),
mediaSourceList, mediaSourceList,
mediaPeriodInfo, mediaPeriodInfo,
emptyTrackSelectorResult); emptyTrackSelectorResult,
preloadConfiguration.targetPreloadDurationUs);
} }
public void experimentalSetForegroundModeTimeoutMs(long setForegroundModeTimeoutMs) { public void experimentalSetForegroundModeTimeoutMs(long setForegroundModeTimeoutMs) {
@ -1213,7 +1215,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
if (!playbackInfo.isLoading if (!playbackInfo.isLoading
&& playbackInfo.totalBufferedDurationUs < PLAYBACK_BUFFER_EMPTY_THRESHOLD_US && 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 // 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 // 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 // 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); MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo);
if (info != null) { if (info != null) {
MediaPeriodHolder mediaPeriodHolder = queue.enqueueNextMediaPeriodHolder(info); 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) { if (queue.getPlayingPeriod() == mediaPeriodHolder) {
resetRendererPosition(info.startPositionUs); resetRendererPosition(info.startPositionUs);
} }
@ -2231,7 +2237,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (shouldContinueLoading) { if (shouldContinueLoading) {
// We should still be loading, except when there is nothing to load or we have fully loaded // We should still be loading, except when there is nothing to load or we have fully loaded
// the current period. // the current period.
shouldContinueLoading = isLoadingPossible(); shouldContinueLoading = isLoadingPossible(queue.getLoadingPeriod());
updateIsLoading(); updateIsLoading();
} else { } else {
maybeContinueLoading(); maybeContinueLoading();
@ -2342,14 +2348,37 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
private void maybeUpdatePreloadPeriods(boolean loadingPeriodChanged) { private void maybeUpdatePreloadPeriods(boolean loadingPeriodChanged) {
if (preloadConfiguration.targetPreloadDurationUs == C.TIME_UNSET if (preloadConfiguration.targetPreloadDurationUs == C.TIME_UNSET) {
|| (!loadingPeriodChanged // Do nothing if preloading disabled.
&& playbackInfo.timeline.equals(lastPreloadPoolInvalidationTimeline))) {
// Do nothing if preloading disabled or no change in loading period or timeline has occurred.
return; return;
} }
lastPreloadPoolInvalidationTimeline = playbackInfo.timeline; if (loadingPeriodChanged
queue.invalidatePreloadPool(playbackInfo.timeline); || !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 { private boolean replaceStreamsOrDisableRendererForTransition() throws ExoPlaybackException {
@ -2531,13 +2560,27 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException { private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException {
if (!queue.isLoading(mediaPeriod)) { if (queue.isLoading(mediaPeriod)) {
// Stale event. handleLoadingPeriodPrepared(checkNotNull(queue.getLoadingPeriod()));
return; } 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( updateLoadControlTrackSelection(
loadingPeriodHolder.info.id, loadingPeriodHolder.info.id,
loadingPeriodHolder.getTrackGroups(), loadingPeriodHolder.getTrackGroups(),
@ -2559,12 +2602,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
private void handleContinueLoadingRequested(MediaPeriod mediaPeriod) { private void handleContinueLoadingRequested(MediaPeriod mediaPeriod) {
if (!queue.isLoading(mediaPeriod)) { if (queue.isLoading(mediaPeriod)) {
// Stale event. queue.reevaluateBuffer(rendererPositionUs);
return; maybeContinueLoading();
} else if (queue.isPreloading(mediaPeriod)) {
maybeContinuePreloading();
} }
queue.reevaluateBuffer(rendererPositionUs);
maybeContinueLoading();
} }
private void handlePlaybackParameters( private void handlePlaybackParameters(
@ -2610,7 +2653,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
private boolean shouldContinueLoading() { private boolean shouldContinueLoading() {
if (!isLoadingPossible()) { if (!isLoadingPossible(queue.getLoadingPeriod())) {
return false; return false;
} }
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
@ -2651,19 +2694,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
return shouldContinueLoading; return shouldContinueLoading;
} }
private boolean isLoadingPossible() { private boolean isLoadingPossible(@Nullable MediaPeriodHolder mediaPeriodHolder) {
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); return mediaPeriodHolder != null
if (loadingPeriodHolder == null) { && !mediaPeriodHolder.hasLoadingError()
return false; && mediaPeriodHolder.getNextLoadPositionUs() != C.TIME_END_OF_SOURCE;
}
if (loadingPeriodHolder.hasLoadingError()) {
return false;
}
long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs();
if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
return false;
}
return true;
} }
private void updateIsLoading() { private void updateIsLoading() {

View File

@ -19,6 +19,7 @@ import androidx.media3.common.C;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.Timeline; import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup; import androidx.media3.common.TrackGroup;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaPeriod;
@ -324,6 +325,24 @@ public interface LoadControl {
throw new IllegalStateException("shouldContinueLoading not implemented"); 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 * 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 * 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; 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. */ /** Whether the media period has finished preparing. */
public boolean prepared; public boolean prepared;
@ -103,13 +109,15 @@ import java.io.IOException;
Allocator allocator, Allocator allocator,
MediaSourceList mediaSourceList, MediaSourceList mediaSourceList,
MediaPeriodInfo info, MediaPeriodInfo info,
TrackSelectorResult emptyTrackSelectorResult) { TrackSelectorResult emptyTrackSelectorResult,
long targetPreloadBufferDurationUs) {
this.rendererCapabilities = rendererCapabilities; this.rendererCapabilities = rendererCapabilities;
this.rendererPositionOffsetUs = rendererPositionOffsetUs; this.rendererPositionOffsetUs = rendererPositionOffsetUs;
this.trackSelector = trackSelector; this.trackSelector = trackSelector;
this.mediaSourceList = mediaSourceList; this.mediaSourceList = mediaSourceList;
this.uid = info.id.periodUid; this.uid = info.id.periodUid;
this.info = info; this.info = info;
this.targetPreloadBufferDurationUs = targetPreloadBufferDurationUs;
this.trackGroups = TrackGroupArray.EMPTY; this.trackGroups = TrackGroupArray.EMPTY;
this.trackSelectorResult = emptyTrackSelectorResult; this.trackSelectorResult = emptyTrackSelectorResult;
sampleStreams = new SampleStream[rendererCapabilities.length]; sampleStreams = new SampleStream[rendererCapabilities.length];
@ -160,6 +168,13 @@ import java.io.IOException;
&& (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE); && (!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 * Returns the buffered position in microseconds. If the period is buffered to the end, then the
* period duration is returned. * period duration is returned.
@ -509,6 +524,11 @@ import java.io.IOException;
&& this.info.id.equals(info.id); && this.info.id.equals(info.id);
} }
public void prepare(MediaPeriod.Callback callback, long startPositionUs) {
prepareCalled = true;
mediaPeriod.prepare(callback, startPositionUs);
}
/* package */ interface Factory { /* package */ interface Factory {
MediaPeriodHolder create(MediaPeriodInfo info, long rendererPositionOffsetUs); MediaPeriodHolder create(MediaPeriodInfo info, long rendererPositionOffsetUs);
} }

View File

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

View File

@ -7196,42 +7196,6 @@ public class ExoPlayerTest {
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */); 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 @Test
public void prepare_preloadingEnabledRepeatModeOne_sameWindowPeriodCreatedForPreloading() public void prepare_preloadingEnabledRepeatModeOne_sameWindowPeriodCreatedForPreloading()
throws Exception { throws Exception {
@ -7243,7 +7207,8 @@ public class ExoPlayerTest {
/* durationUs= */ DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * 2)); /* durationUs= */ DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * 2));
List<MediaPeriodId> createdMediaPeriodIds = new ArrayList<>(); List<MediaPeriodId> createdMediaPeriodIds = new ArrayList<>();
FakeMediaSource mediaSource = FakeMediaSource mediaSource =
new FakeMediaSource(timeline) { new FakeMediaSource(
timeline, ExoPlayerTestRunner.AUDIO_FORMAT, ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override @Override
public MediaPeriod createPeriod( public MediaPeriod createPeriod(
MediaPeriodId id, Allocator allocator, long startPositionUs) { MediaPeriodId id, Allocator allocator, long startPositionUs) {
@ -7270,6 +7235,229 @@ public class ExoPlayerTest {
player.release(); 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 @Test
public void seekToIndexLargerThanNumberOfPlaylistItems() throws Exception { public void seekToIndexLargerThanNumberOfPlaylistItems() throws Exception {
Timeline fakeTimeline = Timeline fakeTimeline =

View File

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