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