diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java index bdfb96014c..5098a451a8 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -437,6 +437,31 @@ public interface ExoPlayer extends Player { default void onOffloadedPlayback(boolean isOffloadedPlayback) {} } + /** Configuration options for preloading playlist items. */ + @UnstableApi + class PreloadConfiguration { + + /** Default preload configuration that disables playlist preloading. */ + public static final PreloadConfiguration DEFAULT = + new PreloadConfiguration(/* targetPreloadDurationUs= */ C.TIME_UNSET); + + /** + * The target duration to buffer when preloading, in microseconds or {@link C#TIME_UNSET} to + * disable preloading. + */ + public final long targetPreloadDurationUs; + + /** + * Creates an instance. + * + * @param targetPreloadDurationUs The target duration to preload, in microseconds or {@link + * C#TIME_UNSET} to disable preloading. + */ + public PreloadConfiguration(long targetPreloadDurationUs) { + this.targetPreloadDurationUs = targetPreloadDurationUs; + } + } + /** * A builder for {@link ExoPlayer} instances. * @@ -1536,6 +1561,19 @@ public interface ExoPlayer extends Player { @UnstableApi void setShuffleOrder(ShuffleOrder shuffleOrder); + /** + * Sets the {@linkplain PreloadConfiguration preload configuration} to configure playlist + * preloading. + * + * @param preloadConfiguration The preload configuration. + */ + @UnstableApi + void setPreloadConfiguration(PreloadConfiguration preloadConfiguration); + + /** Returns the {@linkplain PreloadConfiguration preload configuration}. */ + @UnstableApi + PreloadConfiguration getPreloadConfiguration(); + /** * {@inheritDoc} * diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index 110fbeacf0..9fa44fe736 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -195,6 +195,7 @@ import java.util.concurrent.TimeoutException; private boolean foregroundMode; private SeekParameters seekParameters; private ShuffleOrder shuffleOrder; + private PreloadConfiguration preloadConfiguration; private boolean pauseAtEndOfMediaItems; private Commands availableCommands; private MediaMetadata mediaMetadata; @@ -298,6 +299,7 @@ import java.util.concurrent.TimeoutException; audioOffloadListeners = new CopyOnWriteArraySet<>(); mediaSourceHolderSnapshots = new ArrayList<>(); shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0); + preloadConfiguration = PreloadConfiguration.DEFAULT; emptyTrackSelectorResult = new TrackSelectorResult( new RendererConfiguration[renderers.length], @@ -374,7 +376,8 @@ import java.util.concurrent.TimeoutException; clock, playbackInfoUpdateListener, playerId, - builder.playbackLooper); + builder.playbackLooper, + preloadConfiguration); volume = 1; repeatMode = Player.REPEAT_MODE_OFF; @@ -881,6 +884,21 @@ import java.util.concurrent.TimeoutException; return shuffleModeEnabled; } + @Override + public void setPreloadConfiguration(PreloadConfiguration preloadConfiguration) { + verifyApplicationThread(); + if (this.preloadConfiguration.equals(preloadConfiguration)) { + return; + } + this.preloadConfiguration = preloadConfiguration; + internalPlayer.setPreloadConfiguration(preloadConfiguration); + } + + @Override + public PreloadConfiguration getPreloadConfiguration() { + return preloadConfiguration; + } + @Override public boolean isLoading() { verifyApplicationThread(); 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 201d21f246..a2842d452f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -57,6 +57,7 @@ import androidx.media3.common.util.TraceUtil; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSourceException; import androidx.media3.exoplayer.DefaultMediaClock.PlaybackParametersListener; +import androidx.media3.exoplayer.ExoPlayer.PreloadConfiguration; import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.drm.DrmSession; @@ -171,6 +172,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_ATTEMPT_RENDERER_ERROR_RECOVERY = 25; private static final int MSG_RENDERER_CAPABILITIES_CHANGED = 26; private static final int MSG_UPDATE_MEDIA_SOURCES_WITH_MEDIA_ITEMS = 27; + private static final int MSG_SET_PRELOAD_CONFIGURATION = 28; private static final int ACTIVE_INTERVAL_MS = 10; private static final int IDLE_INTERVAL_MS = 1000; @@ -237,6 +239,8 @@ import java.util.concurrent.atomic.AtomicBoolean; @Nullable private ExoPlaybackException pendingRecoverableRendererError; private long setForegroundModeTimeoutMs; private long playbackMaybeBecameStuckAtMs; + private PreloadConfiguration preloadConfiguration; + private Timeline lastPreloadPoolInvalidationTimeline; public ExoPlayerImplInternal( Renderer[] renderers, @@ -255,7 +259,8 @@ import java.util.concurrent.atomic.AtomicBoolean; Clock clock, PlaybackInfoUpdateListener playbackInfoUpdateListener, PlayerId playerId, - Looper playbackLooper) { + Looper playbackLooper, + PreloadConfiguration preloadConfiguration) { this.playbackInfoUpdateListener = playbackInfoUpdateListener; this.renderers = renderers; this.trackSelector = trackSelector; @@ -271,11 +276,13 @@ import java.util.concurrent.atomic.AtomicBoolean; this.pauseAtEndOfWindow = pauseAtEndOfWindow; this.clock = clock; this.playerId = playerId; + this.preloadConfiguration = preloadConfiguration; playbackMaybeBecameStuckAtMs = C.TIME_UNSET; lastRebufferRealtimeMs = C.TIME_UNSET; backBufferDurationUs = loadControl.getBackBufferDurationUs(playerId); retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(playerId); + lastPreloadPoolInvalidationTimeline = Timeline.EMPTY; playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult); playbackInfoUpdate = new PlaybackInfoUpdate(playbackInfo); @@ -300,7 +307,9 @@ import java.util.concurrent.atomic.AtomicBoolean; deliverPendingMessageAtStartPositionRequired = true; HandlerWrapper eventHandler = clock.createHandler(applicationLooper, /* callback= */ null); - queue = new MediaPeriodQueue(analyticsCollector, eventHandler, this::createMediaPeriodHolder); + queue = + new MediaPeriodQueue( + analyticsCollector, eventHandler, this::createMediaPeriodHolder, preloadConfiguration); mediaSourceList = new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler, playerId); @@ -359,6 +368,10 @@ import java.util.concurrent.atomic.AtomicBoolean; handler.obtainMessage(MSG_SET_SHUFFLE_ENABLED, shuffleModeEnabled ? 1 : 0, 0).sendToTarget(); } + public void setPreloadConfiguration(PreloadConfiguration preloadConfiguration) { + handler.obtainMessage(MSG_SET_PRELOAD_CONFIGURATION, preloadConfiguration).sendToTarget(); + } + public void seekTo(Timeline timeline, int windowIndex, long positionUs) { handler .obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs)) @@ -543,6 +556,9 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_SET_SHUFFLE_ENABLED: setShuffleModeEnabledInternal(msg.arg1 != 0); break; + case MSG_SET_PRELOAD_CONFIGURATION: + setPreloadConfigurationInternal((PreloadConfiguration) msg.obj); + break; case MSG_DO_SOME_WORK: doSomeWork(); break; @@ -930,6 +946,11 @@ import java.util.concurrent.atomic.AtomicBoolean; handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); } + private void setPreloadConfigurationInternal(PreloadConfiguration preloadConfiguration) { + this.preloadConfiguration = preloadConfiguration; + queue.updatePreloadConfiguration(playbackInfo.timeline, preloadConfiguration); + } + private void seekToCurrentPosition(boolean sendDiscontinuity) throws ExoPlaybackException { // Renderers may have read from a period that's been removed. Seek back to the current // position of the playing period to make sure none of the removed period is played. @@ -1605,6 +1626,7 @@ import java.util.concurrent.atomic.AtomicBoolean; /* positionUpdateTimeMs= */ 0, /* sleepingForOffload= */ false); if (releaseMediaSourceList) { + queue.releasePreloadPool(); mediaSourceList.release(); } } @@ -2137,13 +2159,15 @@ import java.util.concurrent.atomic.AtomicBoolean; // No periods available. return; } - maybeUpdateLoadingPeriod(); + boolean loadingPeriodChanged = maybeUpdateLoadingPeriod(); maybeUpdateReadingPeriod(); maybeUpdateReadingRenderers(); maybeUpdatePlayingPeriod(); + maybeUpdatePreloadPeriods(loadingPeriodChanged); } - private void maybeUpdateLoadingPeriod() throws ExoPlaybackException { + private boolean maybeUpdateLoadingPeriod() throws ExoPlaybackException { + boolean loadingPeriodChanged = false; queue.reevaluateBuffer(rendererPositionUs); if (queue.shouldLoadNextMediaPeriod()) { @Nullable @@ -2155,6 +2179,7 @@ import java.util.concurrent.atomic.AtomicBoolean; resetRendererPosition(info.startPositionUs); } handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); + loadingPeriodChanged = true; } } if (shouldContinueLoading) { @@ -2165,6 +2190,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } else { maybeContinueLoading(); } + return loadingPeriodChanged; } private void maybeUpdateReadingPeriod() throws ExoPlaybackException { @@ -2269,6 +2295,17 @@ 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. + return; + } + lastPreloadPoolInvalidationTimeline = playbackInfo.timeline; + queue.invalidatePreloadPool(playbackInfo.timeline); + } + private boolean replaceStreamsOrDisableRendererForTransition() throws ExoPlaybackException { MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult(); 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 99f5d7dffc..64628cfa0c 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java @@ -15,6 +15,7 @@ */ package androidx.media3.exoplayer; +import static androidx.media3.exoplayer.MediaPeriodQueue.areDurationsCompatible; import static java.lang.Math.max; import androidx.annotation.Nullable; @@ -471,6 +472,12 @@ import androidx.media3.exoplayer.upstream.Allocator; } } + public boolean canBeUsedForMediaPeriodInfo(MediaPeriodInfo info) { + return areDurationsCompatible(this.info.durationUs, info.durationUs) + && this.info.startPositionUs == info.startPositionUs + && this.info.id.equals(info.id); + } + /* 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 2da4adcb1c..35c3637963 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java @@ -28,10 +28,13 @@ import androidx.media3.common.Player.RepeatMode; import androidx.media3.common.Timeline; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.HandlerWrapper; +import androidx.media3.exoplayer.ExoPlayer.PreloadConfiguration; import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; /** * Holds a queue of media periods, from the currently playing media period at the front to the @@ -82,6 +85,8 @@ import com.google.common.collect.ImmutableList; private int length; @Nullable private Object oldFrontPeriodUid; private long oldFrontPeriodWindowSequenceNumber; + private PreloadConfiguration preloadConfiguration; + private List preloadPriorityList; /** * Creates a new media period queue. @@ -94,12 +99,15 @@ import com.google.common.collect.ImmutableList; public MediaPeriodQueue( AnalyticsCollector analyticsCollector, HandlerWrapper analyticsCollectorHandler, - MediaPeriodHolder.Factory mediaPeriodHolderFactory) { + MediaPeriodHolder.Factory mediaPeriodHolderFactory, + PreloadConfiguration preloadConfiguration) { this.analyticsCollector = analyticsCollector; this.analyticsCollectorHandler = analyticsCollectorHandler; this.mediaPeriodHolderFactory = mediaPeriodHolderFactory; + this.preloadConfiguration = preloadConfiguration; period = new Timeline.Period(); window = new Timeline.Window(); + preloadPriorityList = new ArrayList<>(); } /** @@ -128,6 +136,18 @@ import com.google.common.collect.ImmutableList; return updateForPlaybackModeChange(timeline); } + /** + * Updates the preload configuration. + * + * @param timeline The current timeline. + * @param preloadConfiguration The new preload configuration. + */ + public void updatePreloadConfiguration( + Timeline timeline, PreloadConfiguration preloadConfiguration) { + this.preloadConfiguration = preloadConfiguration; + invalidatePreloadPool(timeline); + } + /** Returns whether {@code mediaPeriod} is the current loading media period. */ public boolean isLoading(MediaPeriod mediaPeriod) { return loading != null && loading.mediaPeriod == mediaPeriod; @@ -180,8 +200,13 @@ import com.google.common.collect.ImmutableList; loading == null ? INITIAL_RENDERER_POSITION_OFFSET_US : (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs); - MediaPeriodHolder newPeriodHolder = - mediaPeriodHolderFactory.create(info, rendererPositionOffsetUs); + @Nullable MediaPeriodHolder newPeriodHolder = removePreloadedMediaPeriodHolder(info); + if (newPeriodHolder == null) { + newPeriodHolder = mediaPeriodHolderFactory.create(info, rendererPositionOffsetUs); + } else { + newPeriodHolder.info = info; + newPeriodHolder.setRendererOffset(rendererPositionOffsetUs); + } if (loading != null) { loading.setNext(newPeriodHolder); } else { @@ -195,6 +220,110 @@ import com.google.common.collect.ImmutableList; return newPeriodHolder; } + /** Invalidates the preload pool. */ + public void invalidatePreloadPool(Timeline timeline) { + if (preloadConfiguration.targetPreloadDurationUs == C.TIME_UNSET || loading == null) { + releasePreloadPool(); + return; + } + MediaPeriodHolder loading = this.loading; + List newPreloadPriorityList = new ArrayList<>(); + Pair defaultPositionOfNextWindow = + getDefaultPeriodPositionOfNextWindow( + timeline, loading.info.id.periodUid, /* defaultPositionProjectionUs= */ 0L); + if (defaultPositionOfNextWindow != null + && !timeline + .getWindow( + timeline.getPeriodByUid(defaultPositionOfNextWindow.first, period).windowIndex, + window) + .isLive()) { + long windowSequenceNumber = + resolvePeriodUidToWindowSequenceNumberInPreloadPeriods(defaultPositionOfNextWindow.first); + if (windowSequenceNumber == C.INDEX_UNSET) { + windowSequenceNumber = nextWindowSequenceNumber++; + } + @Nullable + MediaPeriodInfo nextInfo = + getMediaPeriodInfoForPeriodPosition( + timeline, + defaultPositionOfNextWindow.first, + defaultPositionOfNextWindow.second, + windowSequenceNumber); + @Nullable + MediaPeriodHolder nextMediaPeriodHolder = removePreloadedMediaPeriodHolder(nextInfo); + if (nextMediaPeriodHolder == null) { + // The holder's renderer position offset may be different and is reset when enqueuing. + long rendererPositionOffsetUs = + loading.getRendererOffset() + loading.info.durationUs - nextInfo.startPositionUs; + nextMediaPeriodHolder = mediaPeriodHolderFactory.create(nextInfo, rendererPositionOffsetUs); + } + newPreloadPriorityList.add(nextMediaPeriodHolder); + } + releaseAndResetPreloadPriorityList(newPreloadPriorityList); + } + + /** Removes all periods from the preload pool and releases them. */ + public void releasePreloadPool() { + if (!preloadPriorityList.isEmpty()) { + releaseAndResetPreloadPriorityList(new ArrayList<>()); + } + } + + @Nullable + private MediaPeriodHolder removePreloadedMediaPeriodHolder(MediaPeriodInfo info) { + for (int i = 0; i < preloadPriorityList.size(); i++) { + MediaPeriodHolder mediaPeriodHolder = preloadPriorityList.get(i); + if (mediaPeriodHolder.canBeUsedForMediaPeriodInfo(info)) { + return preloadPriorityList.remove(i); + } + } + return null; + } + + private void releaseAndResetPreloadPriorityList(List newPriorityList) { + for (int i = 0; i < preloadPriorityList.size(); i++) { + preloadPriorityList.get(i).release(); + } + preloadPriorityList = newPriorityList; + } + + private MediaPeriodInfo getMediaPeriodInfoForPeriodPosition( + Timeline timeline, Object periodUid, long positionUs, long windowSequenceNumber) { + MediaPeriodId mediaPeriodId = + resolveMediaPeriodIdForAds( + timeline, periodUid, positionUs, windowSequenceNumber, window, period); + return mediaPeriodId.isAd() + ? getMediaPeriodInfoForAd( + timeline, + mediaPeriodId.periodUid, + mediaPeriodId.adGroupIndex, + mediaPeriodId.adIndexInAdGroup, + /* contentPositionUs= */ positionUs, + mediaPeriodId.windowSequenceNumber) + : getMediaPeriodInfoForContent( + timeline, + mediaPeriodId.periodUid, + /* startPositionUs= */ positionUs, + /* requestedContentPositionUs= */ C.TIME_UNSET, + mediaPeriodId.windowSequenceNumber); + } + + @Nullable + private Pair getDefaultPeriodPositionOfNextWindow( + Timeline timeline, Object periodUid, long defaultPositionProjectionUs) { + int nextWindowIndex = + timeline.getNextWindowIndex( + timeline.getPeriodByUid(periodUid, period).windowIndex, repeatMode, shuffleModeEnabled); + return nextWindowIndex != C.INDEX_UNSET + ? timeline.getPeriodPositionUs( + window, + period, + nextWindowIndex, + /* windowPositionUs= */ C.TIME_UNSET, + defaultPositionProjectionUs) + : null; + } + /** * Returns the loading period holder which is at the end of the queue, or null if the queue is * empty. @@ -430,7 +559,7 @@ import com.google.common.collect.ImmutableList; */ public MediaPeriodId resolveMediaPeriodIdForAds( Timeline timeline, Object periodUid, long positionUs) { - long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(timeline, periodUid); + long windowSequenceNumber = resolvePeriodUidToWindowSequenceNumber(timeline, periodUid); return resolveMediaPeriodIdForAds( timeline, periodUid, positionUs, windowSequenceNumber, window, period); } @@ -507,7 +636,7 @@ import com.google.common.collect.ImmutableList; */ public MediaPeriodId resolveMediaPeriodIdForAdsAfterPeriodPositionChange( Timeline timeline, Object periodUid, long positionUs) { - long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(timeline, periodUid); + long windowSequenceNumber = resolvePeriodUidToWindowSequenceNumber(timeline, periodUid); // Check for preceding ad periods in multi-period window. timeline.getPeriodByUid(periodUid, period); timeline.getWindow(period.windowIndex, window); @@ -553,7 +682,7 @@ import com.google.common.collect.ImmutableList; * @param periodUid The uid of the timeline period. * @return A window sequence number for a media period created for this timeline period. */ - private long resolvePeriodIndexToWindowSequenceNumber(Timeline timeline, Object periodUid) { + private long resolvePeriodUidToWindowSequenceNumber(Timeline timeline, Object periodUid) { int windowIndex = timeline.getPeriodByUid(periodUid, period).windowIndex; if (oldFrontPeriodUid != null) { int oldFrontPeriodIndex = timeline.getIndexOfPeriod(oldFrontPeriodUid); @@ -585,8 +714,14 @@ import com.google.common.collect.ImmutableList; } mediaPeriodHolder = mediaPeriodHolder.getNext(); } + + long windowSequenceNumber = resolvePeriodUidToWindowSequenceNumberInPreloadPeriods(periodUid); + if (windowSequenceNumber != C.INDEX_UNSET) { + return windowSequenceNumber; + } + // If no match is found, create new sequence number. - long windowSequenceNumber = nextWindowSequenceNumber++; + windowSequenceNumber = nextWindowSequenceNumber++; if (playing == null) { // If the queue is empty, save it as old front uid to allow later reuse. oldFrontPeriodUid = periodUid; @@ -595,6 +730,16 @@ import com.google.common.collect.ImmutableList; return windowSequenceNumber; } + private long resolvePeriodUidToWindowSequenceNumberInPreloadPeriods(Object periodUid) { + for (int i = 0; i < preloadPriorityList.size(); i++) { + MediaPeriodHolder preloadHolder = preloadPriorityList.get(i); + if (preloadHolder.uid.equals(periodUid)) { + return preloadHolder.info.id.windowSequenceNumber; + } + } + return C.INDEX_UNSET; + } + /** * Returns whether a period described by {@code oldInfo} can be kept for playing the media period * described by {@code newInfo}. @@ -606,7 +751,7 @@ import com.google.common.collect.ImmutableList; /** * Returns whether a duration change of a period is compatible with keeping the following periods. */ - private boolean areDurationsCompatible(long previousDurationUs, long newDurationUs) { + /* package */ static boolean areDurationsCompatible(long previousDurationUs, long newDurationUs) { return previousDurationUs == C.TIME_UNSET || previousDurationUs == newDurationUs; } @@ -649,7 +794,6 @@ import com.google.common.collect.ImmutableList; // Update the period info for the last holder, as it may now be the last period in the timeline. lastValidPeriodHolder.info = getUpdatedMediaPeriodInfo(timeline, lastValidPeriodHolder.info); - // If renderers may have read from a period that's been removed, it is necessary to restart. return !readingPeriodRemoved; } @@ -746,7 +890,12 @@ import com.google.common.collect.ImmutableList; if (nextMediaPeriodHolder != null && nextMediaPeriodHolder.uid.equals(nextPeriodUid)) { windowSequenceNumber = nextMediaPeriodHolder.info.id.windowSequenceNumber; } else { - windowSequenceNumber = nextWindowSequenceNumber++; + long windowSequenceNumberFromPreload = + resolvePeriodUidToWindowSequenceNumberInPreloadPeriods(nextPeriodUid); + windowSequenceNumber = + windowSequenceNumberFromPreload == C.INDEX_UNSET + ? nextWindowSequenceNumber++ + : windowSequenceNumberFromPreload; } } @@ -874,7 +1023,6 @@ import com.google.common.collect.ImmutableList; && (adGroupCount > 1 || period.getAdGroupTimeUs(firstAdGroupIndex) != C.TIME_END_OF_SOURCE); } - @Nullable private MediaPeriodInfo getMediaPeriodInfo( Timeline timeline, MediaPeriodId id, long requestedContentPositionUs, long startPositionUs) { timeline.getPeriodByUid(id.periodUid, period); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java index 7b40884687..13d456e981 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java @@ -992,6 +992,18 @@ public class SimpleExoPlayer extends BasePlayer player.setRepeatMode(repeatMode); } + @Override + public void setPreloadConfiguration(PreloadConfiguration preloadConfiguration) { + blockUntilConstructorFinished(); + player.setPreloadConfiguration(preloadConfiguration); + } + + @Override + public PreloadConfiguration getPreloadConfiguration() { + blockUntilConstructorFinished(); + return player.getPreloadConfiguration(); + } + @Override public void setShuffleModeEnabled(boolean shuffleModeEnabled) { blockUntilConstructorFinished(); 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 a16c6bf5e5..b9232ee1f0 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java @@ -40,9 +40,11 @@ import androidx.media3.common.Timeline; import androidx.media3.common.Tracks; import androidx.media3.common.util.Clock; import androidx.media3.common.util.HandlerWrapper; +import androidx.media3.exoplayer.ExoPlayer.PreloadConfiguration; import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector; import androidx.media3.exoplayer.analytics.PlayerId; +import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.MediaSource.MediaSourceCaller; import androidx.media3.exoplayer.source.SinglePeriodTimeline; @@ -61,7 +63,10 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.Test; @@ -95,7 +100,9 @@ public final class MediaPeriodQueueTest { private TrackSelector trackSelector; private Allocator allocator; private MediaSourceList mediaSourceList; - private FakeMediaSource fakeMediaSource; + private List fakeMediaSources; + private ArrayList mediaPeriodHolderFactoryInfos; + private ArrayList mediaPeriodHolderFactoryRendererPositionOffsets; @Before public void setUp() { @@ -105,23 +112,29 @@ public final class MediaPeriodQueueTest { Looper.getMainLooper()); HandlerWrapper handler = Clock.DEFAULT.createHandler(Looper.getMainLooper(), /* callback= */ null); + mediaPeriodHolderFactoryInfos = new ArrayList<>(); + mediaPeriodHolderFactoryRendererPositionOffsets = new ArrayList<>(); mediaPeriodQueue = new MediaPeriodQueue( analyticsCollector, handler, - (info, rendererPositionOffsetUs) -> - new MediaPeriodHolder( - rendererCapabilities, - rendererPositionOffsetUs, - trackSelector, - allocator, - mediaSourceList, - info, - new TrackSelectorResult( - new RendererConfiguration[0], - new ExoTrackSelection[0], - Tracks.EMPTY, - /* info= */ null))); + (info, rendererPositionOffsetUs) -> { + mediaPeriodHolderFactoryInfos.add(info); + mediaPeriodHolderFactoryRendererPositionOffsets.add(rendererPositionOffsetUs); + return new MediaPeriodHolder( + rendererCapabilities, + rendererPositionOffsetUs, + trackSelector, + allocator, + mediaSourceList, + info, + new TrackSelectorResult( + new RendererConfiguration[0], + new ExoTrackSelection[0], + Tracks.EMPTY, + /* info= */ null)); + }, + PreloadConfiguration.DEFAULT); mediaSourceList = new MediaSourceList( mock(MediaSourceList.MediaSourceListInfoRefreshListener.class), @@ -131,6 +144,7 @@ public final class MediaPeriodQueueTest { rendererCapabilities = new RendererCapabilities[0]; trackSelector = mock(TrackSelector.class); allocator = mock(Allocator.class); + fakeMediaSources = new ArrayList<>(); } @Test @@ -305,7 +319,7 @@ public final class MediaPeriodQueueTest { .withContentResumeOffsetUs(/* adGroupIndex= */ 2, /* contentResumeOffsetUs= */ 4000); SinglePeriodAdTimeline adTimeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); - setupTimeline(adTimeline); + setupTimelines(adTimeline); setAdGroupLoaded(/* adGroupIndex= */ 0); assertNextMediaPeriodInfoIsAd( @@ -382,7 +396,7 @@ public final class MediaPeriodQueueTest { .withIsServerSideInserted(/* adGroupIndex= */ 2, /* isServerSideInserted= */ true); SinglePeriodAdTimeline adTimeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); - setupTimeline(adTimeline); + setupTimelines(adTimeline); setAdGroupLoaded(/* adGroupIndex= */ 0); assertNextMediaPeriodInfoIsAd( @@ -463,7 +477,7 @@ public final class MediaPeriodQueueTest { /* isContentTimeline= */ true, /* populateAds= */ false, /* playedAds= */ false); - setupTimeline(multiPeriodLiveTimeline); + setupTimelines(multiPeriodLiveTimeline); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* periodUid= */ firstPeriodUid, /* startPositionUs= */ 0, @@ -533,7 +547,7 @@ public final class MediaPeriodQueueTest { /* isContentTimeline= */ false, /* populateAds= */ false, /* playedAds= */ false); - setupTimeline(multiPeriodLiveTimeline); + setupTimelines(multiPeriodLiveTimeline); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* periodUid= */ firstPeriodUid, /* startPositionUs= */ 0, @@ -602,7 +616,7 @@ public final class MediaPeriodQueueTest { /* isContentTimeline= */ false, /* populateAds= */ true, /* playedAds= */ false); - setupTimeline(multiPeriodLiveTimeline); + setupTimelines(multiPeriodLiveTimeline); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* periodUid= */ firstPeriodUid, /* startPositionUs= */ 0, @@ -686,7 +700,7 @@ public final class MediaPeriodQueueTest { /* isContentTimeline= */ false, /* populateAds= */ true, /* playedAds= */ true); - setupTimeline(multiPeriodLiveTimeline); + setupTimelines(multiPeriodLiveTimeline); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* periodUid= */ firstPeriodUid, /* startPositionUs= */ 0, @@ -798,7 +812,7 @@ public final class MediaPeriodQueueTest { @Test public void getNextMediaPeriodInfo_inMultiPeriodWindow_returnsCorrectMediaPeriodInfos() { - setupTimeline( + setupTimelines( new FakeTimeline( new TimelineWindowDefinition( /* periodCount= */ 2, @@ -896,7 +910,7 @@ public final class MediaPeriodQueueTest { .withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true); SinglePeriodAdTimeline adTimeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); - setupTimeline(adTimeline); + setupTimelines(adTimeline); setAdGroupLoaded(/* adGroupIndex= */ 0); enqueueNext(); // Content before ad. enqueueNext(); // Ad. @@ -907,7 +921,7 @@ public final class MediaPeriodQueueTest { new AdPlaybackState( /* adsId= */ new Object(), /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US - 2000) .withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true); - updateTimeline(); + updateAdTimeline(/* mediaSourceIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0); long maxRendererReadPositionUs = MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US + FIRST_AD_START_TIME_US - 1000; @@ -1397,23 +1411,406 @@ public final class MediaPeriodQueueTest { assertThat(mediaPeriodId.adGroupIndex).isEqualTo(-1); } + @Test + public void invalidatePreloadPool_withThreeWindowsPreloadEnabled_preloadHoldersCreated() { + setupTimelines(new FakeTimeline(), new FakeTimeline(), new FakeTimeline()); + mediaPeriodQueue.updatePreloadConfiguration( + playbackInfo.timeline, new PreloadConfiguration(/* targetPreloadDurationUs= */ 5_000_000L)); + + // Creates period of first window for enqueuing. + enqueueNext(); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(1); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets).containsExactly(1_000_000_000_000L); + assertThat(mediaPeriodHolderFactoryInfos.get(0).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(0)); + assertThat(mediaPeriodHolderFactoryInfos.get(0).id.windowSequenceNumber).isEqualTo(0); + + // Creates period of second window for preloading. + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(2); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets) + .containsExactly(1_000_000_000_000L, 1_000_010_000_000L) + .inOrder(); + assertThat(mediaPeriodHolderFactoryInfos.get(1).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(1)); + assertThat(mediaPeriodHolderFactoryInfos.get(1).id.windowSequenceNumber).isEqualTo(1); + + // Enqueue period of second window from preload pool. + enqueueNext(); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(2); + + // Creates period of third window for preloading. + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(3); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets) + .containsExactly(1_000_000_000_000L, 1_000_010_000_000L, 1_000_020_000_000L) + .inOrder(); + assertThat(mediaPeriodHolderFactoryInfos.get(2).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(2)); + assertThat(mediaPeriodHolderFactoryInfos.get(2).id.windowSequenceNumber).isEqualTo(2); + + // Enqueue period of third window from preload pool. + enqueueNext(); + // No further next window. Invalidating is a no-op. + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(3); + } + + @Test + public void invalidatePreloadPool_withThreeWindowsPreloadDisabled_preloadHoldersNotCreated() { + List releasedMediaPeriods = new ArrayList<>(); + FakeMediaSource fakeMediaSource = + new FakeMediaSource() { + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + releasedMediaPeriods.add(mediaPeriod); + super.releasePeriod(mediaPeriod); + } + }; + setupMediaSources(fakeMediaSource, fakeMediaSource, fakeMediaSource); + mediaPeriodQueue.updatePreloadConfiguration( + playbackInfo.timeline, PreloadConfiguration.DEFAULT); + + enqueueNext(); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(1); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets).containsExactly(1_000_000_000_000L); + assertThat(mediaPeriodHolderFactoryInfos.get(0).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(0)); + assertThat(mediaPeriodHolderFactoryInfos.get(0).id.windowSequenceNumber).isEqualTo(0); + + // Expect no-op. + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(1); + + enqueueNext(); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(2); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets) + .containsExactly(1_000_000_000_000L, 1_000_010_000_000L) + .inOrder(); + assertThat(mediaPeriodHolderFactoryInfos.get(1).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(1)); + assertThat(mediaPeriodHolderFactoryInfos.get(1).id.windowSequenceNumber).isEqualTo(1); + assertThat(releasedMediaPeriods).isEmpty(); + + // Expect no-op. + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(2); + + enqueueNext(); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(3); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets) + .containsExactly(1_000_000_000_000L, 1_000_010_000_000L, 1_000_020_000_000L) + .inOrder(); + assertThat(mediaPeriodHolderFactoryInfos.get(2).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(2)); + assertThat(mediaPeriodHolderFactoryInfos.get(2).id.windowSequenceNumber).isEqualTo(2); + assertThat(releasedMediaPeriods).isEmpty(); + + // Expect no-op. + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(3); + } + + @Test + public void + invalidatePreloadPool_secondWindowIsLivePreloadEnabled_preloadHolderForLiveNotCreated() { + TimelineWindowDefinition liveWindow = + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 1234, + /* isSeekable= */ false, + /* isDynamic= */ true, + /* isLive= */ true, + /* isPlaceholder= */ false, + /* durationUs= */ DEFAULT_WINDOW_DURATION_US, + /* defaultPositionUs= */ 0, + /* windowOffsetInFirstPeriodUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + /* adPlaybackStates= */ ImmutableList.of(AdPlaybackState.NONE), + MediaItem.EMPTY); + setupTimelines(new FakeTimeline(), new FakeTimeline(liveWindow)); + mediaPeriodQueue.updatePreloadConfiguration( + playbackInfo.timeline, new PreloadConfiguration(/* targetPreloadDurationUs= */ 5_000_000L)); + + enqueueNext(); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(1); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets).containsExactly(1_000_000_000_000L); + assertThat(mediaPeriodHolderFactoryInfos.get(0).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(0)); + assertThat(mediaPeriodHolderFactoryInfos.get(0).id.windowSequenceNumber).isEqualTo(0); + + // Expected to be a no-op for live. + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(1); + + enqueueNext(); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(2); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets) + .containsExactly(1_000_000_000_000L, 1_000_010_000_000L) + .inOrder(); + assertThat(mediaPeriodHolderFactoryInfos.get(1).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(1)); + assertThat(mediaPeriodHolderFactoryInfos.get(1).id.windowSequenceNumber).isEqualTo(1); + + // Expected to be a no-op for last window. + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(2); + } + + @Test + public void + invalidatePreloadPool_windowWithTwoPeriodsPreloadEnabled_preloadHolderForThirdPeriodCreated() { + TimelineWindowDefinition window1 = + new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 1234); + setupTimelines(new FakeTimeline(window1), new FakeTimeline()); + mediaPeriodQueue.updatePreloadConfiguration( + playbackInfo.timeline, new PreloadConfiguration(/* targetPreloadDurationUs= */ 5_000_000L)); + + enqueueNext(); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(1); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets).containsExactly(1_000_000_000_000L); + assertThat(mediaPeriodHolderFactoryInfos.get(0).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(/* periodIndex= */ 0)); + assertThat(mediaPeriodHolderFactoryInfos.get(0).id.windowSequenceNumber).isEqualTo(0); + + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(2); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets) + .containsExactly(1_000_000_000_000L, 1_000_005_000_000L) + .inOrder(); + assertThat(mediaPeriodHolderFactoryInfos.get(1).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(/* periodIndex= */ 2)); + assertThat(mediaPeriodHolderFactoryInfos.get(1).id.windowSequenceNumber).isEqualTo(1); + } + + @Test + public void + invalidatePreloadPool_withThreeWindowsWithAdsInSecondPreloadEnabled_preloadHolderCreatedForPreroll() { + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ new Object(), 0L, 5_000_000L) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdCount(/* adGroupIndex= */ 1, 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, 2_000L) + .withAdDurationsUs(/* adGroupIndex= */ 1, 1_000L) + .withContentDurationUs(CONTENT_DURATION_US); + SinglePeriodAdTimeline adTimeline = + new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); + setupMediaSources( + new FakeMediaSource(), new FakeMediaSource(adTimeline), new FakeMediaSource()); + mediaPeriodQueue.updatePreloadConfiguration( + playbackInfo.timeline, new PreloadConfiguration(/* targetPreloadDurationUs= */ 5_000_000L)); + + // Creates the first and only period of the first window for enqueuing. + enqueueNext(); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(1); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets).containsExactly(1_000_000_000_000L); + assertThat(mediaPeriodHolderFactoryInfos.get(0).id.windowSequenceNumber).isEqualTo(0); + assertThat(mediaPeriodHolderFactoryInfos.get(0).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(0)); + assertThat(mediaPeriodHolderFactoryInfos.get(0).id.adGroupIndex).isEqualTo(-1); + assertThat(mediaPeriodHolderFactoryInfos.get(0).id.adIndexInAdGroup).isEqualTo(-1); + assertThat(mediaPeriodHolderFactoryInfos.get(0).id.nextAdGroupIndex).isEqualTo(-1); + + // Creates the pre-roll period of the 2nd window for preload. + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(2); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets) + .containsExactly(1_000_000_000_000L, 1_000_133_000_000L) + .inOrder(); + assertThat(mediaPeriodHolderFactoryInfos.get(1).id.windowSequenceNumber).isEqualTo(1); + assertThat(mediaPeriodHolderFactoryInfos.get(1).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(/* periodIndex= */ 1)); + assertThat(mediaPeriodHolderFactoryInfos.get(1).id.adGroupIndex).isEqualTo(0); + assertThat(mediaPeriodHolderFactoryInfos.get(1).id.adIndexInAdGroup).isEqualTo(0); + assertThat(mediaPeriodHolderFactoryInfos.get(1).id.nextAdGroupIndex).isEqualTo(-1); + + // Enqueue the pre-roll period from pool. + enqueueNext(); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(2); + + // Creates the first content period of the 3rd window for preloading. + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(3); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets) + .containsExactly( + 1_000_000_000_000L, + 1_000_133_000_000L, + 1_000_133_000_000L + 2_000 - DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US) + .inOrder(); + assertThat(mediaPeriodHolderFactoryInfos.get(2).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(/* periodIndex= */ 2)); + assertThat(mediaPeriodHolderFactoryInfos.get(2).id.adGroupIndex).isEqualTo(-1); + assertThat(mediaPeriodHolderFactoryInfos.get(2).id.adIndexInAdGroup).isEqualTo(-1); + assertThat(mediaPeriodHolderFactoryInfos.get(2).id.nextAdGroupIndex).isEqualTo(-1); + assertThat(mediaPeriodHolderFactoryInfos.get(2).id.windowSequenceNumber).isEqualTo(2); + + // Creates the first content period of the 2nd window for enqueueing. + enqueueNext(); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(4); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets) + .containsExactly( + 1_000_000_000_000L, + 1_000_133_000_000L, + 1_000_133_000_000L + 2_000L - DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + 1_000_133_000_000L + 2_000L) + .inOrder(); + assertThat(mediaPeriodHolderFactoryInfos.get(3).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(/* periodIndex= */ 1)); + assertThat(mediaPeriodHolderFactoryInfos.get(3).id.adGroupIndex).isEqualTo(-1); + assertThat(mediaPeriodHolderFactoryInfos.get(3).id.adIndexInAdGroup).isEqualTo(-1); + assertThat(mediaPeriodHolderFactoryInfos.get(3).id.nextAdGroupIndex).isEqualTo(1); + assertThat(mediaPeriodHolderFactoryInfos.get(3).id.windowSequenceNumber).isEqualTo(1); + + // Invalidating does keep the same state and does not create further periods. + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(4); + + // Creates the mid-roll ad period of the 2nd window for enqueueing. + enqueueNext(); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(5); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets) + .containsExactly( + 1_000_000_000_000L, + 1_000_133_000_000L, + 1_000_133_002_000L - DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + 1_000_133_002_000L, + 1_000_138_002_000L); + assertThat(mediaPeriodHolderFactoryInfos.get(4).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(/* periodIndex= */ 1)); + assertThat(mediaPeriodHolderFactoryInfos.get(4).id.adGroupIndex).isEqualTo(1); + assertThat(mediaPeriodHolderFactoryInfos.get(4).id.adIndexInAdGroup).isEqualTo(0); + assertThat(mediaPeriodHolderFactoryInfos.get(4).id.nextAdGroupIndex).isEqualTo(-1); + assertThat(mediaPeriodHolderFactoryInfos.get(4).id.windowSequenceNumber).isEqualTo(1); + + // Invalidating does keep the same state and does not create further periods. + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(5); + + // Creates the last content period of the 2nd window for enqueueing. + enqueueNext(); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(6); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets) + .containsExactly( + 1_000_000_000_000L, + 1_000_133_000_000L, + 1_000_133_002_000L - DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + 1_000_133_002_000L, + 1_000_138_002_000L, + 1_000_133_003_000L); + assertThat(mediaPeriodHolderFactoryInfos.get(5).id.periodUid) + .isEqualTo(playbackInfo.timeline.getUidOfPeriod(/* periodIndex= */ 1)); + assertThat(mediaPeriodHolderFactoryInfos.get(5).id.adGroupIndex).isEqualTo(-1); + assertThat(mediaPeriodHolderFactoryInfos.get(5).id.adIndexInAdGroup).isEqualTo(-1); + assertThat(mediaPeriodHolderFactoryInfos.get(5).id.nextAdGroupIndex).isEqualTo(-1); + assertThat(mediaPeriodHolderFactoryInfos.get(5).id.windowSequenceNumber).isEqualTo(1); + + // Invalidating does keep the same state and does not create further periods. + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + // Enqueue the first and only content period of the 3rd and last window from pool. + enqueueNext(); + // No further next window. Invalidating is a no-op. + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + + assertThat(mediaPeriodHolderFactoryInfos).hasSize(6); + } + + @Test + public void setPreloadConfiguration_disablePreloading_releasesPreloadHolders() { + AtomicBoolean releaseCalled = new AtomicBoolean(); + FakeMediaSource preloadedSource = + new FakeMediaSource( + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 1, "1234"))) { + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + releaseCalled.set(true); + super.releasePeriod(mediaPeriod); + } + }; + setupMediaSources(new FakeMediaSource(), preloadedSource); + mediaPeriodQueue.updatePreloadConfiguration( + playbackInfo.timeline, new PreloadConfiguration(/* targetPreloadDurationUs= */ 5_000_000L)); + enqueueNext(); + mediaPeriodQueue.invalidatePreloadPool(playbackInfo.timeline); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets) + .containsExactly(1_000_000_000_000L, 1_000_010_000_000L) + .inOrder(); + assertThat(releaseCalled.get()).isFalse(); + + mediaPeriodQueue.updatePreloadConfiguration( + playbackInfo.timeline, PreloadConfiguration.DEFAULT); + + assertThat(releaseCalled.get()).isTrue(); + } + + @Test + public void setPreloadConfiguration_enablePreloading_preloadHolderCreated() { + setupTimelines(new FakeTimeline(), new FakeTimeline()); + enqueueNext(); + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets).containsExactly(1_000_000_000_000L); + + mediaPeriodQueue.updatePreloadConfiguration( + playbackInfo.timeline, new PreloadConfiguration(/* targetPreloadDurationUs= */ 5_000_000L)); + + assertThat(mediaPeriodHolderFactoryRendererPositionOffsets) + .containsExactly(1_000_000_000_000L, 1_000_010_000_000L) + .inOrder(); + } + private void setupAdTimeline(long... adGroupTimesUs) { adPlaybackState = new AdPlaybackState(/* adsId= */ new Object(), adGroupTimesUs) .withContentDurationUs(CONTENT_DURATION_US); SinglePeriodAdTimeline adTimeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); - setupTimeline(adTimeline); + setupTimelines(adTimeline); } - private void setupTimeline(Timeline timeline) { - fakeMediaSource = new FakeMediaSource(timeline); - MediaSourceList.MediaSourceHolder mediaSourceHolder = - new MediaSourceList.MediaSourceHolder(fakeMediaSource, /* useLazyPreparation= */ false); - mediaSourceList.setMediaSources( - ImmutableList.of(mediaSourceHolder), new FakeShuffleOrder(/* length= */ 1)); - mediaSourceHolder.mediaSource.prepareSource( - mock(MediaSourceCaller.class), /* mediaTransferListener= */ null, PlayerId.UNSET); + private void setupTimelines(Timeline... timelines) { + FakeMediaSource[] sources = new FakeMediaSource[timelines.length]; + for (int i = 0; i < timelines.length; i++) { + sources[i] = new FakeMediaSource(timelines[i]); + } + setupMediaSources(sources); + } + + private void setupMediaSources(FakeMediaSource... mediaSources) { + ImmutableList.Builder mediaSourceHolders = + new ImmutableList.Builder<>(); + for (FakeMediaSource source : mediaSources) { + fakeMediaSources.add(source); + MediaSourceList.MediaSourceHolder mediaSourceHolder = + new MediaSourceList.MediaSourceHolder(source, /* useLazyPreparation= */ false); + mediaSourceHolder.mediaSource.prepareSource( + mock(MediaSourceCaller.class), /* mediaTransferListener= */ null, PlayerId.UNSET); + mediaSourceHolders.add(mediaSourceHolder); + } + ImmutableList holders = mediaSourceHolders.build(); + mediaSourceList.setMediaSources(holders, new FakeShuffleOrder(/* length= */ holders.size())); Timeline playlistTimeline = mediaSourceList.createTimeline(); firstPeriodUid = playlistTimeline.getUidOfPeriod(/* periodIndex= */ 0); @@ -1494,14 +1891,14 @@ public final class MediaPeriodQueueTest { .withAdCount(adGroupIndex, /* adCount= */ 1) .withAvailableAdMediaItem(adGroupIndex, /* adIndexInAdGroup= */ 0, AD_MEDIA_ITEM) .withAdDurationsUs(newDurations); - updateTimeline(); + updateAdTimeline(/* mediaSourceIndex= */ 0); } private void setAdGroupPlayed(int adGroupIndex) { for (int i = 0; i < adPlaybackState.getAdGroup(adGroupIndex).count; i++) { adPlaybackState = adPlaybackState.withPlayedAd(adGroupIndex, /* adIndexInAdGroup= */ i); } - updateTimeline(); + updateAdTimeline(/* mediaSourceIndex= */ 0); } private void setAdGroupFailedToLoad(int adGroupIndex) { @@ -1509,20 +1906,20 @@ public final class MediaPeriodQueueTest { adPlaybackState .withAdCount(adGroupIndex, /* adCount= */ 1) .withAdLoadError(adGroupIndex, /* adIndexInAdGroup= */ 0); - updateTimeline(); + updateAdTimeline(/* mediaSourceIndex= */ 0); } private void updateAdPlaybackStateAndTimeline(long... adGroupTimesUs) { adPlaybackState = new AdPlaybackState(/* adsId= */ new Object(), adGroupTimesUs) .withContentDurationUs(CONTENT_DURATION_US); - updateTimeline(); + updateAdTimeline(/* mediaSourceIndex= */ 0); } - private void updateTimeline() { + private void updateAdTimeline(int mediaSourceIndex) { SinglePeriodAdTimeline adTimeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); - fakeMediaSource.setNewSourceInfo(adTimeline); + fakeMediaSources.get(mediaSourceIndex).setNewSourceInfo(adTimeline); // Progress the looper so that the source info events have been executed. shadowOf(Looper.getMainLooper()).idle(); playbackInfo = playbackInfo.copyWithTimeline(mediaSourceList.createTimeline()); diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java index 2900aeb040..be84bb2ce9 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java @@ -154,6 +154,16 @@ public class StubExoPlayer extends StubPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public void setPreloadConfiguration(PreloadConfiguration preloadConfiguration) { + throw new UnsupportedOperationException(); + } + + @Override + public PreloadConfiguration getPreloadConfiguration() { + throw new UnsupportedOperationException(); + } + @Override public void setMediaSource(MediaSource mediaSource) { throw new UnsupportedOperationException();