diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 49cd436624..cec580ab4b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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: 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 9066566b7a..46323d748b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultLoadControl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultLoadControl.java @@ -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}. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index 7bab7d178a..3db0d8439b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -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() { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadControl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadControl.java index 3602250be6..1ae9ccb89e 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadControl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadControl.java @@ -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 diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java index f3b0b8f520..e5721c40c9 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java @@ -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); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java index 35c3637963..b68d2c4877 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java @@ -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 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; } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index 8b349c4a21..df66adc42e 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -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 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 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 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 preloadPreparationPositionUs = new ArrayList<>(); + List 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 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 preloadPreparationPositionUs = new ArrayList<>(); + List 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 = diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java index 166f099c40..fc59770ddd 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java @@ -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 =