Add preload pool to media period MediaPeriodQueue

The pool is created and the queue uses holder instances found
to enqueue. No preloading is done yet though.

PiperOrigin-RevId: 631053172
This commit is contained in:
bachinger 2024-05-06 08:04:46 -07:00 committed by Copybara-Service
parent 4a54db7cc7
commit 0ab6ea5668
8 changed files with 721 additions and 54 deletions

View File

@ -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}
*

View File

@ -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();

View File

@ -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();

View File

@ -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);
}

View File

@ -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<MediaPeriodHolder> 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<MediaPeriodHolder> newPreloadPriorityList = new ArrayList<>();
Pair<Object, Long> 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<MediaPeriodHolder> 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<Object, Long> 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);

View File

@ -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();

View File

@ -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<FakeMediaSource> fakeMediaSources;
private ArrayList<MediaPeriodInfo> mediaPeriodHolderFactoryInfos;
private ArrayList<Long> 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<MediaPeriod> 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<MediaSourceList.MediaSourceHolder> 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<MediaSourceList.MediaSourceHolder> 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());

View File

@ -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();