Preload first period of next window
Allow apps to preload the first period of the next window in the playlist of `ExoPlayer`. By default playlist preloading is disabled. To enable preloading, `ExoPlayer.setPreloadConfiguration(PreloadConfiguration)` can be called. `LoadControl` determines when to preload with its implemenation of `shouldContinuePreloading(timeline, mediaPeriodId, bufferedDurationUs)`. The implementation in `DefaultLoadControl` allows preloading only when the player isn't currently loading for playback. Apps can override this behaviour. Issue: androidx/media#468 PiperOrigin-RevId: 677786017
This commit is contained in:
parent
3d3ec85c12
commit
ba1cdba403
@ -34,6 +34,17 @@
|
||||
* Add `ForwardingRenderer` implementation that forwards all method calls
|
||||
to another renderer
|
||||
([1703](https://github.com/androidx/media/pull/1703)).
|
||||
* Add playlist preloading for the next item in the playlist. Apps can
|
||||
enable preloading by calling
|
||||
`ExoPlayer.setPreloadConfiguration(PreloadConfiguration)` accordingly.
|
||||
By default preloading is disabled. When opted-in and to not interfer
|
||||
with playback, `DefaultLoadControl` restricts preloading to start and
|
||||
continue only when the player is not loading for playback. Apps can
|
||||
change this behaviour by implementing
|
||||
`LoadControl.shouldContinuePreloading()` accordingly (like when
|
||||
overriding this method in `DefaultLoadControl`). The default
|
||||
implementation of `LoadControl` disables preloading in case an app is
|
||||
using a custom implementation of `LoadControl`.
|
||||
* Transformer:
|
||||
* Track Selection:
|
||||
* Extractors:
|
||||
|
@ -422,6 +422,17 @@ public class DefaultLoadControl implements LoadControl {
|
||||
&& allocator.getTotalBytesAllocated() >= calculateTotalTargetBufferBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldContinuePreloading(
|
||||
Timeline timeline, MediaPeriodId mediaPeriodId, long bufferedDurationUs) {
|
||||
for (PlayerLoadingState playerLoadingState : loadingStates.values()) {
|
||||
if (playerLoadingState.isLoading) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate target buffer size in bytes based on the selected tracks. The player will try not to
|
||||
* exceed this target buffer. Only used when {@code targetBufferBytes} is {@link C#LENGTH_UNSET}.
|
||||
|
@ -16,6 +16,7 @@
|
||||
package androidx.media3.exoplayer;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
import static androidx.media3.common.util.Util.castNonNull;
|
||||
import static androidx.media3.common.util.Util.msToUs;
|
||||
import static androidx.media3.exoplayer.Renderer.STATE_DISABLED;
|
||||
@ -340,7 +341,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
loadControl.getAllocator(),
|
||||
mediaSourceList,
|
||||
mediaPeriodInfo,
|
||||
emptyTrackSelectorResult);
|
||||
emptyTrackSelectorResult,
|
||||
preloadConfiguration.targetPreloadDurationUs);
|
||||
}
|
||||
|
||||
public void experimentalSetForegroundModeTimeoutMs(long setForegroundModeTimeoutMs) {
|
||||
@ -1213,7 +1215,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
}
|
||||
if (!playbackInfo.isLoading
|
||||
&& playbackInfo.totalBufferedDurationUs < PLAYBACK_BUFFER_EMPTY_THRESHOLD_US
|
||||
&& isLoadingPossible()) {
|
||||
&& isLoadingPossible(queue.getLoadingPeriod())) {
|
||||
// The renderers are not ready, there is more media available to load, and the LoadControl
|
||||
// is refusing to load it (indicated by !playbackInfo.isLoading). This could be because the
|
||||
// renderers are still transitioning to their ready states, but it could also indicate a
|
||||
@ -2220,7 +2222,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo);
|
||||
if (info != null) {
|
||||
MediaPeriodHolder mediaPeriodHolder = queue.enqueueNextMediaPeriodHolder(info);
|
||||
mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
|
||||
if (!mediaPeriodHolder.prepareCalled) {
|
||||
mediaPeriodHolder.prepare(this, info.startPositionUs);
|
||||
} else if (mediaPeriodHolder.prepared) {
|
||||
handler.obtainMessage(MSG_PERIOD_PREPARED, mediaPeriodHolder.mediaPeriod).sendToTarget();
|
||||
}
|
||||
if (queue.getPlayingPeriod() == mediaPeriodHolder) {
|
||||
resetRendererPosition(info.startPositionUs);
|
||||
}
|
||||
@ -2231,7 +2237,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
if (shouldContinueLoading) {
|
||||
// We should still be loading, except when there is nothing to load or we have fully loaded
|
||||
// the current period.
|
||||
shouldContinueLoading = isLoadingPossible();
|
||||
shouldContinueLoading = isLoadingPossible(queue.getLoadingPeriod());
|
||||
updateIsLoading();
|
||||
} else {
|
||||
maybeContinueLoading();
|
||||
@ -2342,14 +2348,37 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
}
|
||||
|
||||
private void maybeUpdatePreloadPeriods(boolean loadingPeriodChanged) {
|
||||
if (preloadConfiguration.targetPreloadDurationUs == C.TIME_UNSET
|
||||
|| (!loadingPeriodChanged
|
||||
&& playbackInfo.timeline.equals(lastPreloadPoolInvalidationTimeline))) {
|
||||
// Do nothing if preloading disabled or no change in loading period or timeline has occurred.
|
||||
if (preloadConfiguration.targetPreloadDurationUs == C.TIME_UNSET) {
|
||||
// Do nothing if preloading disabled.
|
||||
return;
|
||||
}
|
||||
lastPreloadPoolInvalidationTimeline = playbackInfo.timeline;
|
||||
queue.invalidatePreloadPool(playbackInfo.timeline);
|
||||
if (loadingPeriodChanged
|
||||
|| !playbackInfo.timeline.equals(lastPreloadPoolInvalidationTimeline)) {
|
||||
// invalidate the pool when the loading period or the timeline changed.
|
||||
lastPreloadPoolInvalidationTimeline = playbackInfo.timeline;
|
||||
queue.invalidatePreloadPool(playbackInfo.timeline);
|
||||
}
|
||||
maybeContinuePreloading();
|
||||
}
|
||||
|
||||
private void maybeContinuePreloading() {
|
||||
queue.maybeUpdatePreloadMediaPeriodHolder();
|
||||
MediaPeriodHolder preloading = queue.getPreloadingPeriod();
|
||||
if (preloading == null
|
||||
|| (preloading.prepareCalled && !preloading.prepared)
|
||||
|| preloading.mediaPeriod.isLoading()
|
||||
|| !loadControl.shouldContinuePreloading(
|
||||
playbackInfo.timeline,
|
||||
preloading.info.id,
|
||||
preloading.prepared ? preloading.mediaPeriod.getBufferedPositionUs() : 0L)) {
|
||||
return;
|
||||
}
|
||||
if (!preloading.prepareCalled) {
|
||||
preloading.prepare(/* callback= */ this, preloading.info.startPositionUs);
|
||||
} else {
|
||||
preloading.continueLoading(
|
||||
rendererPositionUs, playbackInfo.playbackParameters.speed, lastRebufferRealtimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean replaceStreamsOrDisableRendererForTransition() throws ExoPlaybackException {
|
||||
@ -2531,13 +2560,27 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
}
|
||||
|
||||
private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException {
|
||||
if (!queue.isLoading(mediaPeriod)) {
|
||||
// Stale event.
|
||||
return;
|
||||
if (queue.isLoading(mediaPeriod)) {
|
||||
handleLoadingPeriodPrepared(checkNotNull(queue.getLoadingPeriod()));
|
||||
} else {
|
||||
@Nullable MediaPeriodHolder preloadHolder = queue.getPreloadHolderByMediaPeriod(mediaPeriod);
|
||||
if (preloadHolder != null) {
|
||||
checkState(!preloadHolder.prepared);
|
||||
preloadHolder.handlePrepared(
|
||||
mediaClock.getPlaybackParameters().speed, playbackInfo.timeline);
|
||||
if (queue.isPreloading(mediaPeriod)) {
|
||||
maybeContinuePreloading();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLoadingPeriodPrepared(MediaPeriodHolder loadingPeriodHolder)
|
||||
throws ExoPlaybackException {
|
||||
if (!loadingPeriodHolder.prepared) {
|
||||
loadingPeriodHolder.handlePrepared(
|
||||
mediaClock.getPlaybackParameters().speed, playbackInfo.timeline);
|
||||
}
|
||||
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
|
||||
loadingPeriodHolder.handlePrepared(
|
||||
mediaClock.getPlaybackParameters().speed, playbackInfo.timeline);
|
||||
updateLoadControlTrackSelection(
|
||||
loadingPeriodHolder.info.id,
|
||||
loadingPeriodHolder.getTrackGroups(),
|
||||
@ -2559,12 +2602,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
}
|
||||
|
||||
private void handleContinueLoadingRequested(MediaPeriod mediaPeriod) {
|
||||
if (!queue.isLoading(mediaPeriod)) {
|
||||
// Stale event.
|
||||
return;
|
||||
if (queue.isLoading(mediaPeriod)) {
|
||||
queue.reevaluateBuffer(rendererPositionUs);
|
||||
maybeContinueLoading();
|
||||
} else if (queue.isPreloading(mediaPeriod)) {
|
||||
maybeContinuePreloading();
|
||||
}
|
||||
queue.reevaluateBuffer(rendererPositionUs);
|
||||
maybeContinueLoading();
|
||||
}
|
||||
|
||||
private void handlePlaybackParameters(
|
||||
@ -2610,7 +2653,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
}
|
||||
|
||||
private boolean shouldContinueLoading() {
|
||||
if (!isLoadingPossible()) {
|
||||
if (!isLoadingPossible(queue.getLoadingPeriod())) {
|
||||
return false;
|
||||
}
|
||||
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
|
||||
@ -2651,19 +2694,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
return shouldContinueLoading;
|
||||
}
|
||||
|
||||
private boolean isLoadingPossible() {
|
||||
MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
|
||||
if (loadingPeriodHolder == null) {
|
||||
return false;
|
||||
}
|
||||
if (loadingPeriodHolder.hasLoadingError()) {
|
||||
return false;
|
||||
}
|
||||
long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs();
|
||||
if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
private boolean isLoadingPossible(@Nullable MediaPeriodHolder mediaPeriodHolder) {
|
||||
return mediaPeriodHolder != null
|
||||
&& !mediaPeriodHolder.hasLoadingError()
|
||||
&& mediaPeriodHolder.getNextLoadPositionUs() != C.TIME_END_OF_SOURCE;
|
||||
}
|
||||
|
||||
private void updateIsLoading() {
|
||||
|
@ -19,6 +19,7 @@ import androidx.media3.common.C;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.Timeline;
|
||||
import androidx.media3.common.TrackGroup;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.analytics.PlayerId;
|
||||
import androidx.media3.exoplayer.source.MediaPeriod;
|
||||
@ -324,6 +325,24 @@ public interface LoadControl {
|
||||
throw new IllegalStateException("shouldContinueLoading not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to determine whether preloading should be continued. If this method returns true, the
|
||||
* presented period will continue to load media.
|
||||
*
|
||||
* @param timeline The Timeline containing the preload period that can be looked up with
|
||||
* MediaPeriodId.periodUid.
|
||||
* @param mediaPeriodId The MediaPeriodId of the preloading period.
|
||||
* @param bufferedDurationUs The duration of media currently buffered by the preload period.
|
||||
* @return Whether the preloading should continue for the given period.
|
||||
*/
|
||||
default boolean shouldContinuePreloading(
|
||||
Timeline timeline, MediaPeriodId mediaPeriodId, long bufferedDurationUs) {
|
||||
Log.w(
|
||||
"LoadControl",
|
||||
"shouldContinuePreloading needs to be implemented when playlist preloading is enabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called repeatedly by the player when it's loading the source, has yet to start playback, and
|
||||
* has the minimum amount of data necessary for playback to be started. The value returned
|
||||
|
@ -53,6 +53,12 @@ import java.io.IOException;
|
||||
*/
|
||||
public final @NullableType SampleStream[] sampleStreams;
|
||||
|
||||
/** The target buffer duration to preload. */
|
||||
public final long targetPreloadBufferDurationUs;
|
||||
|
||||
/** Whether {@link #prepare(MediaPeriod.Callback, long)} has been called. */
|
||||
public boolean prepareCalled;
|
||||
|
||||
/** Whether the media period has finished preparing. */
|
||||
public boolean prepared;
|
||||
|
||||
@ -103,13 +109,15 @@ import java.io.IOException;
|
||||
Allocator allocator,
|
||||
MediaSourceList mediaSourceList,
|
||||
MediaPeriodInfo info,
|
||||
TrackSelectorResult emptyTrackSelectorResult) {
|
||||
TrackSelectorResult emptyTrackSelectorResult,
|
||||
long targetPreloadBufferDurationUs) {
|
||||
this.rendererCapabilities = rendererCapabilities;
|
||||
this.rendererPositionOffsetUs = rendererPositionOffsetUs;
|
||||
this.trackSelector = trackSelector;
|
||||
this.mediaSourceList = mediaSourceList;
|
||||
this.uid = info.id.periodUid;
|
||||
this.info = info;
|
||||
this.targetPreloadBufferDurationUs = targetPreloadBufferDurationUs;
|
||||
this.trackGroups = TrackGroupArray.EMPTY;
|
||||
this.trackSelectorResult = emptyTrackSelectorResult;
|
||||
sampleStreams = new SampleStream[rendererCapabilities.length];
|
||||
@ -160,6 +168,13 @@ import java.io.IOException;
|
||||
&& (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE);
|
||||
}
|
||||
|
||||
/** Returns whether the period is fully preloaded. */
|
||||
public boolean isFullyPreloaded() {
|
||||
return prepared
|
||||
&& (isFullyBuffered()
|
||||
|| getBufferedPositionUs() - info.startPositionUs >= targetPreloadBufferDurationUs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the buffered position in microseconds. If the period is buffered to the end, then the
|
||||
* period duration is returned.
|
||||
@ -509,6 +524,11 @@ import java.io.IOException;
|
||||
&& this.info.id.equals(info.id);
|
||||
}
|
||||
|
||||
public void prepare(MediaPeriod.Callback callback, long startPositionUs) {
|
||||
prepareCalled = true;
|
||||
mediaPeriod.prepare(callback, startPositionUs);
|
||||
}
|
||||
|
||||
/* package */ interface Factory {
|
||||
MediaPeriodHolder create(MediaPeriodInfo info, long rendererPositionOffsetUs);
|
||||
}
|
||||
|
@ -79,13 +79,14 @@ import java.util.List;
|
||||
private long nextWindowSequenceNumber;
|
||||
private @RepeatMode int repeatMode;
|
||||
private boolean shuffleModeEnabled;
|
||||
private PreloadConfiguration preloadConfiguration;
|
||||
@Nullable private MediaPeriodHolder playing;
|
||||
@Nullable private MediaPeriodHolder reading;
|
||||
@Nullable private MediaPeriodHolder loading;
|
||||
@Nullable private MediaPeriodHolder preloading;
|
||||
private int length;
|
||||
@Nullable private Object oldFrontPeriodUid;
|
||||
private long oldFrontPeriodWindowSequenceNumber;
|
||||
private PreloadConfiguration preloadConfiguration;
|
||||
private List<MediaPeriodHolder> preloadPriorityList;
|
||||
|
||||
/**
|
||||
@ -153,6 +154,11 @@ import java.util.List;
|
||||
return loading != null && loading.mediaPeriod == mediaPeriod;
|
||||
}
|
||||
|
||||
/** Returns whether {@code mediaPeriod} is the current preloading media period. */
|
||||
public boolean isPreloading(MediaPeriod mediaPeriod) {
|
||||
return preloading != null && preloading.mediaPeriod == mediaPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is a loading period, reevaluates its buffer.
|
||||
*
|
||||
@ -285,6 +291,8 @@ import java.util.List;
|
||||
preloadPriorityList.get(i).release();
|
||||
}
|
||||
preloadPriorityList = newPriorityList;
|
||||
preloading = null;
|
||||
maybeUpdatePreloadMediaPeriodHolder();
|
||||
}
|
||||
|
||||
private MediaPeriodInfo getMediaPeriodInfoForPeriodPosition(
|
||||
@ -333,6 +341,12 @@ import java.util.List;
|
||||
return loading;
|
||||
}
|
||||
|
||||
/** Returns the preloading period holder, or null if there is no preloading period. */
|
||||
@Nullable
|
||||
public MediaPeriodHolder getPreloadingPeriod() {
|
||||
return preloading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the playing period holder which is at the front of the queue, or null if the queue is
|
||||
* empty.
|
||||
@ -414,6 +428,35 @@ import java.util.List;
|
||||
return removedReading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preloading period to the next period in the queue to preload or to null, if all
|
||||
* periods in the preload pool are fully loaded.
|
||||
*/
|
||||
public void maybeUpdatePreloadMediaPeriodHolder() {
|
||||
if (preloading != null && !preloading.isFullyPreloaded()) {
|
||||
return;
|
||||
}
|
||||
preloading = null;
|
||||
for (int i = 0; i < preloadPriorityList.size(); i++) {
|
||||
MediaPeriodHolder mediaPeriodHolder = preloadPriorityList.get(i);
|
||||
if (!mediaPeriodHolder.isFullyPreloaded()) {
|
||||
preloading = mediaPeriodHolder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public MediaPeriodHolder getPreloadHolderByMediaPeriod(MediaPeriod mediaPeriod) {
|
||||
for (int i = 0; i < preloadPriorityList.size(); i++) {
|
||||
MediaPeriodHolder mediaPeriodHolder = preloadPriorityList.get(i);
|
||||
if (mediaPeriodHolder.mediaPeriod == mediaPeriod) {
|
||||
return mediaPeriodHolder;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Clears the queue. */
|
||||
public void clear() {
|
||||
if (length == 0) {
|
||||
@ -734,6 +777,7 @@ import java.util.List;
|
||||
for (int i = 0; i < preloadPriorityList.size(); i++) {
|
||||
MediaPeriodHolder preloadHolder = preloadPriorityList.get(i);
|
||||
if (preloadHolder.uid.equals(periodUid)) {
|
||||
// Found a match in the preload periods.
|
||||
return preloadHolder.info.id.windowSequenceNumber;
|
||||
}
|
||||
}
|
||||
|
@ -7196,42 +7196,6 @@ public class ExoPlayerTest {
|
||||
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void prepare_preloadingEnabled_nextWindowPeriodCreatedForPreloading() throws Exception {
|
||||
FakeMediaSource mediaSource1 =
|
||||
new FakeMediaSource(
|
||||
new FakeTimeline(
|
||||
new TimelineWindowDefinition(
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ false,
|
||||
/* durationUs= */ DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * 2)));
|
||||
List<MediaPeriodId> createdMediaPeriodIds = new ArrayList<>();
|
||||
FakeMediaSource mediaSource2 =
|
||||
new FakeMediaSource() {
|
||||
@Override
|
||||
public MediaPeriod createPeriod(
|
||||
MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
createdMediaPeriodIds.add(id);
|
||||
return super.createPeriod(id, allocator, startPositionUs);
|
||||
}
|
||||
};
|
||||
ExoPlayer player =
|
||||
// Intentionally not using `parameterizeTestExoPlayerBuilder()` for preload specific test.
|
||||
new TestExoPlayerBuilder(context)
|
||||
.setPreloadConfiguration(
|
||||
new ExoPlayer.PreloadConfiguration(/* targetPreloadDurationUs= */ 5_000_000L))
|
||||
.build();
|
||||
player.setMediaSources(ImmutableList.of(mediaSource1, mediaSource2));
|
||||
|
||||
player.prepare();
|
||||
run(player).untilPendingCommandsAreFullyHandled();
|
||||
|
||||
assertThat(createdMediaPeriodIds).hasSize(1);
|
||||
play(player).untilState(Player.STATE_ENDED);
|
||||
assertThat(createdMediaPeriodIds).hasSize(1);
|
||||
player.release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void prepare_preloadingEnabledRepeatModeOne_sameWindowPeriodCreatedForPreloading()
|
||||
throws Exception {
|
||||
@ -7243,7 +7207,8 @@ public class ExoPlayerTest {
|
||||
/* durationUs= */ DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * 2));
|
||||
List<MediaPeriodId> createdMediaPeriodIds = new ArrayList<>();
|
||||
FakeMediaSource mediaSource =
|
||||
new FakeMediaSource(timeline) {
|
||||
new FakeMediaSource(
|
||||
timeline, ExoPlayerTestRunner.AUDIO_FORMAT, ExoPlayerTestRunner.VIDEO_FORMAT) {
|
||||
@Override
|
||||
public MediaPeriod createPeriod(
|
||||
MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
@ -7270,6 +7235,229 @@ public class ExoPlayerTest {
|
||||
player.release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void prepare_preloadingEnabled_nextWindowPeriodPreloaded() throws Exception {
|
||||
List<MediaPeriodId> createdMediaPeriodIds = new ArrayList<>();
|
||||
FakeMediaSource mediaSource1 =
|
||||
new FakeMediaSource(
|
||||
new FakeTimeline(
|
||||
new TimelineWindowDefinition(
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ false,
|
||||
/* durationUs= */ DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * 2)),
|
||||
ExoPlayerTestRunner.AUDIO_FORMAT,
|
||||
ExoPlayerTestRunner.VIDEO_FORMAT) {
|
||||
@Override
|
||||
protected MediaPeriod createMediaPeriod(
|
||||
MediaPeriodId id,
|
||||
TrackGroupArray trackGroupArray,
|
||||
Allocator allocator,
|
||||
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
|
||||
DrmSessionManager drmSessionManager,
|
||||
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
|
||||
@Nullable TransferListener transferListener) {
|
||||
createdMediaPeriodIds.add(id);
|
||||
return super.createMediaPeriod(
|
||||
id,
|
||||
trackGroupArray,
|
||||
allocator,
|
||||
mediaSourceEventDispatcher,
|
||||
drmSessionManager,
|
||||
drmEventDispatcher,
|
||||
transferListener);
|
||||
}
|
||||
};
|
||||
List<Long> preloadPreparationPositionUs = new ArrayList<>();
|
||||
List<LoadingInfo> preloadLoadingInfos = new ArrayList<>();
|
||||
FakeMediaSource mediaSource2 =
|
||||
new FakeMediaSource(
|
||||
new FakeTimeline(),
|
||||
ExoPlayerTestRunner.AUDIO_FORMAT,
|
||||
ExoPlayerTestRunner.VIDEO_FORMAT) {
|
||||
@Override
|
||||
protected MediaPeriod createMediaPeriod(
|
||||
MediaPeriodId id,
|
||||
TrackGroupArray trackGroupArray,
|
||||
Allocator allocator,
|
||||
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
|
||||
DrmSessionManager drmSessionManager,
|
||||
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
|
||||
@Nullable TransferListener transferListener) {
|
||||
createdMediaPeriodIds.add(id);
|
||||
long positionInWindowUs =
|
||||
getTimeline()
|
||||
.getPeriodByUid(id.periodUid, new Timeline.Period())
|
||||
.getPositionInWindowUs();
|
||||
long defaultFirstSampleTimeUs = positionInWindowUs >= 0 ? 0 : -positionInWindowUs;
|
||||
return new FakeMediaPeriod(
|
||||
trackGroupArray,
|
||||
allocator,
|
||||
FakeMediaPeriod.TrackDataFactory.singleSampleWithTimeUs(defaultFirstSampleTimeUs),
|
||||
mediaSourceEventDispatcher,
|
||||
drmSessionManager,
|
||||
drmEventDispatcher,
|
||||
/* deferOnPrepared= */ false) {
|
||||
@Override
|
||||
public synchronized void prepare(Callback callback, long positionUs) {
|
||||
preloadPreparationPositionUs.add(positionUs);
|
||||
super.prepare(callback, positionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean continueLoading(LoadingInfo loadingInfo) {
|
||||
preloadLoadingInfos.add(loadingInfo);
|
||||
return super.continueLoading(loadingInfo);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
MediaPeriodId firstMediaPeriodId =
|
||||
new MediaPeriodId(/* periodUid= */ new Pair<>(0, 0), /* windowSequenceNumber= */ 0);
|
||||
MediaPeriodId secondMediaPeriodId =
|
||||
new MediaPeriodId(/* periodUid= */ new Pair<>(0, 0), /* windowSequenceNumber= */ 1);
|
||||
ExoPlayer player =
|
||||
// Intentionally not using `parameterizeTestExoPlayerBuilder()` for preload specific test.
|
||||
new TestExoPlayerBuilder(context)
|
||||
.setLoadControl(
|
||||
new DefaultLoadControl() {
|
||||
@Override
|
||||
public boolean shouldContinuePreloading(
|
||||
Timeline timeline, MediaPeriodId mediaPeriodId, long bufferedDurationUs) {
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.setPreloadConfiguration(
|
||||
new ExoPlayer.PreloadConfiguration(/* targetPreloadDurationUs= */ 5_000_000L))
|
||||
.build();
|
||||
player.setMediaSources(ImmutableList.of(mediaSource1, mediaSource2));
|
||||
|
||||
player.prepare();
|
||||
run(player).untilPendingCommandsAreFullyHandled();
|
||||
|
||||
// Assert both media periods are created, prepared and loaded when paused after preparation.
|
||||
assertThat(createdMediaPeriodIds)
|
||||
.containsExactly(firstMediaPeriodId, secondMediaPeriodId)
|
||||
.inOrder();
|
||||
assertThat(preloadPreparationPositionUs).containsExactly(123_000_000L);
|
||||
assertThat(preloadLoadingInfos).hasSize(1);
|
||||
|
||||
play(player).untilState(Player.STATE_ENDED);
|
||||
|
||||
assertThat(createdMediaPeriodIds)
|
||||
.containsExactly(firstMediaPeriodId, secondMediaPeriodId)
|
||||
.inOrder();
|
||||
// Verify that the preloaded period from the pool was used for enqueuing.
|
||||
assertThat(preloadPreparationPositionUs).containsExactly(123_000_000L);
|
||||
assertThat(preloadLoadingInfos).hasSize(1);
|
||||
player.release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void prepare_preloadingDisabled_nextWindowPeriodNotPreloaded() throws Exception {
|
||||
List<MediaPeriodId> createdMediaPeriodIds = new ArrayList<>();
|
||||
FakeMediaSource mediaSource1 =
|
||||
new FakeMediaSource(
|
||||
new FakeTimeline(
|
||||
new TimelineWindowDefinition(
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ false,
|
||||
/* durationUs= */ DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * 2)),
|
||||
ExoPlayerTestRunner.AUDIO_FORMAT,
|
||||
ExoPlayerTestRunner.VIDEO_FORMAT) {
|
||||
@Override
|
||||
protected MediaPeriod createMediaPeriod(
|
||||
MediaPeriodId id,
|
||||
TrackGroupArray trackGroupArray,
|
||||
Allocator allocator,
|
||||
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
|
||||
DrmSessionManager drmSessionManager,
|
||||
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
|
||||
@Nullable TransferListener transferListener) {
|
||||
createdMediaPeriodIds.add(id);
|
||||
return super.createMediaPeriod(
|
||||
id,
|
||||
trackGroupArray,
|
||||
allocator,
|
||||
mediaSourceEventDispatcher,
|
||||
drmSessionManager,
|
||||
drmEventDispatcher,
|
||||
transferListener);
|
||||
}
|
||||
};
|
||||
List<Long> preloadPreparationPositionUs = new ArrayList<>();
|
||||
List<LoadingInfo> preloadLoadingInfos = new ArrayList<>();
|
||||
FakeMediaSource mediaSource2 =
|
||||
new FakeMediaSource(
|
||||
new FakeTimeline(),
|
||||
ExoPlayerTestRunner.AUDIO_FORMAT,
|
||||
ExoPlayerTestRunner.VIDEO_FORMAT) {
|
||||
@Override
|
||||
protected MediaPeriod createMediaPeriod(
|
||||
MediaPeriodId id,
|
||||
TrackGroupArray trackGroupArray,
|
||||
Allocator allocator,
|
||||
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
|
||||
DrmSessionManager drmSessionManager,
|
||||
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
|
||||
@Nullable TransferListener transferListener) {
|
||||
createdMediaPeriodIds.add(id);
|
||||
long positionInWindowUs =
|
||||
getTimeline()
|
||||
.getPeriodByUid(id.periodUid, new Timeline.Period())
|
||||
.getPositionInWindowUs();
|
||||
long defaultFirstSampleTimeUs = positionInWindowUs >= 0 ? 0 : -positionInWindowUs;
|
||||
return new FakeMediaPeriod(
|
||||
trackGroupArray,
|
||||
allocator,
|
||||
FakeMediaPeriod.TrackDataFactory.singleSampleWithTimeUs(defaultFirstSampleTimeUs),
|
||||
mediaSourceEventDispatcher,
|
||||
drmSessionManager,
|
||||
drmEventDispatcher,
|
||||
/* deferOnPrepared= */ false) {
|
||||
@Override
|
||||
public synchronized void prepare(Callback callback, long positionUs) {
|
||||
preloadPreparationPositionUs.add(positionUs);
|
||||
super.prepare(callback, positionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean continueLoading(LoadingInfo loadingInfo) {
|
||||
preloadLoadingInfos.add(loadingInfo);
|
||||
return super.continueLoading(loadingInfo);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
MediaPeriodId firstMediaPeriodId =
|
||||
new MediaPeriodId(/* periodUid= */ new Pair<>(0, 0), /* windowSequenceNumber= */ 0);
|
||||
MediaPeriodId secondMediaPeriodId =
|
||||
new MediaPeriodId(/* periodUid= */ new Pair<>(0, 0), /* windowSequenceNumber= */ 1);
|
||||
ExoPlayer player =
|
||||
// Intentionally not using `parameterizeTestExoPlayerBuilder()` for preload specific test.
|
||||
new TestExoPlayerBuilder(context)
|
||||
.setPreloadConfiguration(ExoPlayer.PreloadConfiguration.DEFAULT)
|
||||
.build();
|
||||
player.setMediaSources(ImmutableList.of(mediaSource1, mediaSource2));
|
||||
|
||||
player.prepare();
|
||||
run(player).untilPendingCommandsAreFullyHandled();
|
||||
|
||||
// Assert the media period of the second source isn't created yet.
|
||||
assertThat(createdMediaPeriodIds).containsExactly(firstMediaPeriodId);
|
||||
assertThat(preloadPreparationPositionUs).isEmpty();
|
||||
assertThat(preloadLoadingInfos).isEmpty();
|
||||
|
||||
play(player).untilState(Player.STATE_ENDED);
|
||||
|
||||
// Assert the second second period is created for playback only.
|
||||
assertThat(createdMediaPeriodIds)
|
||||
.containsExactly(firstMediaPeriodId, secondMediaPeriodId)
|
||||
.inOrder();
|
||||
assertThat(preloadPreparationPositionUs).containsExactly(123_000_000L);
|
||||
assertThat(preloadLoadingInfos).hasSize(1);
|
||||
player.release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seekToIndexLargerThanNumberOfPlaylistItems() throws Exception {
|
||||
Timeline fakeTimeline =
|
||||
|
@ -132,7 +132,8 @@ public final class MediaPeriodQueueTest {
|
||||
new RendererConfiguration[0],
|
||||
new ExoTrackSelection[0],
|
||||
Tracks.EMPTY,
|
||||
/* info= */ null));
|
||||
/* info= */ null),
|
||||
/* targetPreloadBufferDurationUs= */ 5_000_000L);
|
||||
},
|
||||
PreloadConfiguration.DEFAULT);
|
||||
mediaSourceList =
|
||||
|
Loading…
x
Reference in New Issue
Block a user