diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index c3202547a2..e4610387ad 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -8,6 +8,12 @@
is preloaded again.
* Apply the correct corresponding `TrackSelectionResult` to the playing
period in track reselection.
+ * Add `BasePreloadManager` which coordinates the preloading for multiple
+ sources based on the priorities defined by their `rankingData`.
+ Customization is possible by extending this class. Add
+ `DefaultPreloadManager` which uses `PreloadMediaSource` to preload media
+ samples of the sources into memory, and uses an integer `rankingData`
+ that indicates the index of an item on the UI.
* Transformer:
* Add `audioConversionProcess` and `videoConversionProcess` to
`ExportResult` indicating how the respective track in the output file
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRendererCapabilitiesList.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRendererCapabilitiesList.java
new file mode 100644
index 0000000000..729581eb6a
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRendererCapabilitiesList.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media3.exoplayer;
+
+import android.content.Context;
+import androidx.media3.common.util.SystemClock;
+import androidx.media3.common.util.UnstableApi;
+import androidx.media3.common.util.Util;
+import androidx.media3.exoplayer.analytics.PlayerId;
+import androidx.media3.exoplayer.audio.AudioRendererEventListener;
+import androidx.media3.exoplayer.video.VideoRendererEventListener;
+import java.util.Arrays;
+
+/** The default {@link RendererCapabilitiesList} implementation. */
+@UnstableApi
+public final class DefaultRendererCapabilitiesList implements RendererCapabilitiesList {
+
+ /** Factory for {@link DefaultRendererCapabilitiesList}. */
+ public static final class Factory implements RendererCapabilitiesList.Factory {
+ private final RenderersFactory renderersFactory;
+
+ /**
+ * Creates an instance.
+ *
+ * @param context A context to create a {@link DefaultRenderersFactory} that is used as the
+ * default.
+ */
+ public Factory(Context context) {
+ this.renderersFactory = new DefaultRenderersFactory(context);
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param renderersFactory The {@link RenderersFactory} to create an array of {@linkplain
+ * Renderer renderers} whose {@link RendererCapabilities} are represented by the {@link
+ * DefaultRendererCapabilitiesList}.
+ */
+ public Factory(RenderersFactory renderersFactory) {
+ this.renderersFactory = renderersFactory;
+ }
+
+ @Override
+ public DefaultRendererCapabilitiesList createRendererCapabilitiesList() {
+ Renderer[] renderers =
+ renderersFactory.createRenderers(
+ Util.createHandlerForCurrentLooper(),
+ new VideoRendererEventListener() {},
+ new AudioRendererEventListener() {},
+ cueGroup -> {},
+ metadata -> {});
+ return new DefaultRendererCapabilitiesList(renderers);
+ }
+ }
+
+ private final Renderer[] renderers;
+
+ private DefaultRendererCapabilitiesList(Renderer[] renderers) {
+ this.renderers = Arrays.copyOf(renderers, renderers.length);
+ for (int i = 0; i < renderers.length; i++) {
+ this.renderers[i].init(i, PlayerId.UNSET, SystemClock.DEFAULT);
+ }
+ }
+
+ @Override
+ public RendererCapabilities[] getRendererCapabilities() {
+ RendererCapabilities[] rendererCapabilities = new RendererCapabilities[renderers.length];
+ for (int i = 0; i < renderers.length; i++) {
+ rendererCapabilities[i] = renderers[i].getCapabilities();
+ }
+ return rendererCapabilities;
+ }
+
+ @Override
+ public void release() {
+ for (Renderer renderer : renderers) {
+ renderer.release();
+ }
+ }
+}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilitiesList.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilitiesList.java
new file mode 100644
index 0000000000..4ecbde15d2
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilitiesList.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media3.exoplayer;
+
+import androidx.media3.common.util.UnstableApi;
+
+/** A list of {@link RendererCapabilities}. */
+@UnstableApi
+public interface RendererCapabilitiesList {
+
+ /** A factory for {@link RendererCapabilitiesList} instances. */
+ interface Factory {
+
+ /** Creates a {@link RendererCapabilitiesList} instance. */
+ RendererCapabilitiesList createRendererCapabilitiesList();
+ }
+
+ /** Returns an array of {@link RendererCapabilities}. */
+ RendererCapabilities[] getRendererCapabilities();
+
+ /** Releases any resources associated with this {@link RendererCapabilitiesList}. */
+ void release();
+}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/BasePreloadManager.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/BasePreloadManager.java
new file mode 100644
index 0000000000..10bfba34f4
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/BasePreloadManager.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media3.exoplayer.source.preload;
+
+import static androidx.media3.common.util.Assertions.checkNotNull;
+
+import android.os.Handler;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import androidx.media3.common.C;
+import androidx.media3.common.MediaItem;
+import androidx.media3.common.util.UnstableApi;
+import androidx.media3.common.util.Util;
+import androidx.media3.exoplayer.source.MediaSource;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.PriorityQueue;
+
+/**
+ * A base implementation of a preload manager, which maintains the lifecycle of {@linkplain
+ * MediaSource media sources}.
+ *
+ *
Methods should be called on the same thread.
+ */
+@UnstableApi
+public abstract class BasePreloadManager {
+
+ /** A base class of the builder of the concrete extension of {@link BasePreloadManager}. */
+ protected abstract static class BuilderBase {
+
+ protected final Comparator rankingDataComparator;
+ protected final TargetPreloadStatusControl targetPreloadStatusControl;
+ protected final MediaSource.Factory mediaSourceFactory;
+
+ public BuilderBase(
+ Comparator rankingDataComparator,
+ TargetPreloadStatusControl targetPreloadStatusControl,
+ MediaSource.Factory mediaSourceFactory) {
+ this.rankingDataComparator = rankingDataComparator;
+ this.targetPreloadStatusControl = targetPreloadStatusControl;
+ this.mediaSourceFactory = mediaSourceFactory;
+ }
+
+ public abstract BasePreloadManager build();
+ }
+
+ private final Object lock;
+ protected final Comparator rankingDataComparator;
+ private final TargetPreloadStatusControl targetPreloadStatusControl;
+ private final MediaSource.Factory mediaSourceFactory;
+ private final Map mediaItemMediaSourceHolderMap;
+ private final Handler startPreloadingHandler;
+
+ @GuardedBy("lock")
+ private final PriorityQueue sourceHolderPriorityQueue;
+
+ @GuardedBy("lock")
+ @Nullable
+ private TargetPreloadStatusControl.PreloadStatus targetPreloadStatusOfCurrentPreloadingSource;
+
+ protected BasePreloadManager(
+ Comparator rankingDataComparator,
+ TargetPreloadStatusControl targetPreloadStatusControl,
+ MediaSource.Factory mediaSourceFactory) {
+ lock = new Object();
+ this.rankingDataComparator = rankingDataComparator;
+ this.targetPreloadStatusControl = targetPreloadStatusControl;
+ this.mediaSourceFactory = mediaSourceFactory;
+ mediaItemMediaSourceHolderMap = new HashMap<>();
+ startPreloadingHandler = Util.createHandlerForCurrentOrMainLooper();
+ sourceHolderPriorityQueue = new PriorityQueue<>();
+ }
+
+ /**
+ * Gets the count of the {@linkplain MediaSource media sources} currently being managed by the
+ * preload manager.
+ *
+ * @return The count of the {@linkplain MediaSource media sources}.
+ */
+ public final int getSourceCount() {
+ return mediaItemMediaSourceHolderMap.size();
+ }
+
+ /**
+ * Adds a {@link MediaItem} with its {@code rankingData} to the preload manager.
+ *
+ * @param mediaItem The {@link MediaItem} to add.
+ * @param rankingData The ranking data that is associated with the {@code mediaItem}.
+ */
+ public final void add(MediaItem mediaItem, T rankingData) {
+ add(mediaSourceFactory.createMediaSource(mediaItem), rankingData);
+ }
+
+ /**
+ * Adds a {@link MediaSource} with its {@code rankingData} to the preload manager.
+ *
+ * @param mediaSource The {@link MediaSource} to add.
+ * @param rankingData The ranking data that is associated with the {@code mediaSource}.
+ */
+ public final void add(MediaSource mediaSource, T rankingData) {
+ MediaSource mediaSourceForPreloading = createMediaSourceForPreloading(mediaSource);
+ MediaSourceHolder mediaSourceHolder =
+ new MediaSourceHolder(mediaSourceForPreloading, rankingData);
+ mediaItemMediaSourceHolderMap.put(mediaSourceForPreloading.getMediaItem(), mediaSourceHolder);
+ }
+
+ /**
+ * Invalidates the current preload progress, and triggers a new preload progress based on the new
+ * priorities of the managed {@linkplain MediaSource media sources}.
+ */
+ public final void invalidate() {
+ synchronized (lock) {
+ sourceHolderPriorityQueue.clear();
+ sourceHolderPriorityQueue.addAll(mediaItemMediaSourceHolderMap.values());
+ maybeStartPreloadNextSource();
+ }
+ }
+
+ /**
+ * Returns the {@link MediaSource} for the given {@link MediaItem}.
+ *
+ * @param mediaItem The media item.
+ * @return The source for the given {@code mediaItem} if it is managed by the preload manager,
+ * null otherwise.
+ */
+ @Nullable
+ public final MediaSource getMediaSource(MediaItem mediaItem) {
+ if (!mediaItemMediaSourceHolderMap.containsKey(mediaItem)) {
+ return null;
+ }
+ return mediaItemMediaSourceHolderMap.get(mediaItem).mediaSource;
+ }
+
+ /**
+ * Removes a {@link MediaItem} from the preload manager.
+ *
+ * @param mediaItem The {@link MediaItem} to remove.
+ */
+ public final void remove(MediaItem mediaItem) {
+ if (mediaItemMediaSourceHolderMap.containsKey(mediaItem)) {
+ MediaSource mediaSource = mediaItemMediaSourceHolderMap.get(mediaItem).mediaSource;
+ mediaItemMediaSourceHolderMap.remove(mediaItem);
+ releaseSourceInternal(mediaSource);
+ }
+ }
+
+ /** Releases the preload manager. */
+ public final void release() {
+ for (MediaSourceHolder sourceHolder : mediaItemMediaSourceHolderMap.values()) {
+ releaseSourceInternal(sourceHolder.mediaSource);
+ }
+ mediaItemMediaSourceHolderMap.clear();
+ synchronized (lock) {
+ sourceHolderPriorityQueue.clear();
+ targetPreloadStatusOfCurrentPreloadingSource = null;
+ }
+ releaseInternal();
+ }
+
+ /** Called when the given {@link MediaSource} completes to preload. */
+ protected final void onPreloadCompleted(MediaSource source) {
+ startPreloadingHandler.post(
+ () -> {
+ synchronized (lock) {
+ if (sourceHolderPriorityQueue.isEmpty()
+ || checkNotNull(sourceHolderPriorityQueue.peek()).mediaSource != source) {
+ return;
+ }
+ sourceHolderPriorityQueue.poll();
+ maybeStartPreloadNextSource();
+ }
+ });
+ }
+
+ /**
+ * Returns the {@linkplain TargetPreloadStatusControl.PreloadStatus target preload status} of the
+ * given {@link MediaSource}.
+ */
+ @Nullable
+ protected final TargetPreloadStatusControl.PreloadStatus getTargetPreloadStatus(
+ MediaSource source) {
+ synchronized (lock) {
+ if (sourceHolderPriorityQueue.isEmpty()
+ || checkNotNull(sourceHolderPriorityQueue.peek()).mediaSource != source) {
+ return null;
+ }
+ return targetPreloadStatusOfCurrentPreloadingSource;
+ }
+ }
+
+ /**
+ * Returns the {@link MediaSource} that the preload manager creates for preloading based on the
+ * given {@link MediaSource source}. The default implementation returns the same source.
+ *
+ * @param mediaSource The source based on which the preload manager creates for preloading.
+ * @return The source the preload manager creates for preloading.
+ */
+ protected MediaSource createMediaSourceForPreloading(MediaSource mediaSource) {
+ return mediaSource;
+ }
+
+ /** Returns whether the next {@link MediaSource} should start preloading. */
+ protected boolean shouldStartPreloadingNextSource() {
+ return true;
+ }
+
+ /**
+ * Preloads the given {@link MediaSource}.
+ *
+ * @param mediaSource The media source to preload.
+ * @param startPositionsUs The expected starting position in microseconds, or {@link C#TIME_UNSET}
+ * to indicate the default start position.
+ */
+ protected abstract void preloadSourceInternal(MediaSource mediaSource, long startPositionsUs);
+
+ /**
+ * Releases the given {@link MediaSource}.
+ *
+ * @param mediaSource The media source to release.
+ */
+ protected abstract void releaseSourceInternal(MediaSource mediaSource);
+
+ /** Releases the preload manager, see {@link #release()}. */
+ protected void releaseInternal() {}
+
+ @GuardedBy("lock")
+ private void maybeStartPreloadNextSource() {
+ if (!sourceHolderPriorityQueue.isEmpty() && shouldStartPreloadingNextSource()) {
+ MediaSourceHolder preloadingHolder = checkNotNull(sourceHolderPriorityQueue.peek());
+ this.targetPreloadStatusOfCurrentPreloadingSource =
+ targetPreloadStatusControl.getTargetPreloadStatus(preloadingHolder.rankingData);
+ preloadSourceInternal(preloadingHolder.mediaSource, preloadingHolder.startPositionUs);
+ }
+ }
+
+ /** A holder for information for preloading a single media source. */
+ private final class MediaSourceHolder implements Comparable {
+
+ public final MediaSource mediaSource;
+ public final T rankingData;
+ public final long startPositionUs;
+
+ public MediaSourceHolder(MediaSource mediaSource, T rankingData) {
+ this(mediaSource, rankingData, C.TIME_UNSET);
+ }
+
+ public MediaSourceHolder(MediaSource mediaSource, T rankingData, long startPositionUs) {
+ this.mediaSource = mediaSource;
+ this.rankingData = rankingData;
+ this.startPositionUs = startPositionUs;
+ }
+
+ @Override
+ public int compareTo(BasePreloadManager.MediaSourceHolder o) {
+ return rankingDataComparator.compare(this.rankingData, o.rankingData);
+ }
+ }
+}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/DefaultPreloadManager.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/DefaultPreloadManager.java
new file mode 100644
index 0000000000..19b6532a49
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/DefaultPreloadManager.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media3.exoplayer.source.preload;
+
+import static androidx.media3.common.util.Assertions.checkArgument;
+import static androidx.media3.common.util.Assertions.checkNotNull;
+import static androidx.media3.common.util.Assertions.checkState;
+import static java.lang.Math.abs;
+import static java.lang.annotation.ElementType.TYPE_USE;
+
+import android.os.Looper;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.media3.common.C;
+import androidx.media3.common.Timeline;
+import androidx.media3.common.util.UnstableApi;
+import androidx.media3.common.util.Util;
+import androidx.media3.exoplayer.ExoPlayer;
+import androidx.media3.exoplayer.Renderer;
+import androidx.media3.exoplayer.RendererCapabilitiesList;
+import androidx.media3.exoplayer.RenderersFactory;
+import androidx.media3.exoplayer.source.MediaSource;
+import androidx.media3.exoplayer.source.SampleQueue;
+import androidx.media3.exoplayer.trackselection.TrackSelector;
+import androidx.media3.exoplayer.upstream.Allocator;
+import androidx.media3.exoplayer.upstream.BandwidthMeter;
+import com.google.common.base.Predicate;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Comparator;
+
+/**
+ * A preload manager that preloads with the {@link PreloadMediaSource} to load the media data into
+ * the {@link SampleQueue}.
+ */
+@UnstableApi
+public final class DefaultPreloadManager extends BasePreloadManager {
+
+ /**
+ * An implementation of {@link TargetPreloadStatusControl.PreloadStatus} that describes the
+ * preload status of the {@link PreloadMediaSource}.
+ */
+ public static class Status implements TargetPreloadStatusControl.PreloadStatus {
+
+ /**
+ * Stages that for the preload status. One of {@link #STAGE_TIMELINE_REFRESHED}, {@link
+ * #STAGE_SOURCE_PREPARED} or {@link #STAGE_LOADED_TO_POSITION_MS}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @Target(TYPE_USE)
+ @IntDef(
+ value = {
+ STAGE_TIMELINE_REFRESHED,
+ STAGE_SOURCE_PREPARED,
+ STAGE_LOADED_TO_POSITION_MS,
+ })
+ public @interface Stage {}
+
+ /** The {@link PreloadMediaSource} has its {@link Timeline} refreshed. */
+ public static final int STAGE_TIMELINE_REFRESHED = 0;
+
+ /** The {@link PreloadMediaSource} is prepared. */
+ public static final int STAGE_SOURCE_PREPARED = 1;
+
+ /** The {@link PreloadMediaSource} is loaded to a specific position in microseconds. */
+ public static final int STAGE_LOADED_TO_POSITION_MS = 2;
+
+ private final @Stage int stage;
+ private final long value;
+
+ public Status(@Stage int stage, long value) {
+ this.stage = stage;
+ this.value = value;
+ }
+
+ public Status(@Stage int stage) {
+ this(stage, C.TIME_UNSET);
+ }
+
+ @Override
+ public @Stage int getStage() {
+ return stage;
+ }
+
+ @Override
+ public long getValue() {
+ return value;
+ }
+ }
+
+ private final RendererCapabilitiesList rendererCapabilitiesList;
+ private final PreloadMediaSource.Factory preloadMediaSourceFactory;
+
+ /**
+ * Constructs a new instance.
+ *
+ * @param targetPreloadStatusControl The {@link TargetPreloadStatusControl}.
+ * @param mediaSourceFactory The {@link MediaSource.Factory}.
+ * @param trackSelector The {@link TrackSelector}. The instance passed should be {@link
+ * TrackSelector#init(TrackSelector.InvalidationListener, BandwidthMeter) initialized}.
+ * @param bandwidthMeter The {@link BandwidthMeter}. It should be the same bandwidth meter of the
+ * {@link ExoPlayer} that will play the managed {@link PreloadMediaSource}.
+ * @param rendererCapabilitiesListFactory The {@link RendererCapabilitiesList.Factory}. To make
+ * preloading work properly, it must create a {@link RendererCapabilitiesList} holding an
+ * {@linkplain RendererCapabilitiesList#getRendererCapabilities() array of renderer
+ * capabilities} that matches the {@linkplain ExoPlayer#getRendererCount() count} and the
+ * {@linkplain ExoPlayer#getRendererType(int) renderer types} of the array of {@linkplain
+ * Renderer renderers} created by the {@link RenderersFactory} used by the {@link ExoPlayer}
+ * that will play the managed {@link PreloadMediaSource}.
+ * @param allocator The {@link Allocator}. It should be the same allocator of the {@link
+ * ExoPlayer} that will play the managed {@link PreloadMediaSource}.
+ * @param preloadLooper The {@link Looper} that will be used for preloading. It should be the same
+ * playback looper of the {@link ExoPlayer} that will play the manager {@link
+ * PreloadMediaSource}.
+ */
+ public DefaultPreloadManager(
+ TargetPreloadStatusControl targetPreloadStatusControl,
+ MediaSource.Factory mediaSourceFactory,
+ TrackSelector trackSelector,
+ BandwidthMeter bandwidthMeter,
+ RendererCapabilitiesList.Factory rendererCapabilitiesListFactory,
+ Allocator allocator,
+ Looper preloadLooper) {
+ super(new RankingDataComparator(), targetPreloadStatusControl, mediaSourceFactory);
+ this.rendererCapabilitiesList =
+ rendererCapabilitiesListFactory.createRendererCapabilitiesList();
+ preloadMediaSourceFactory =
+ new PreloadMediaSource.Factory(
+ mediaSourceFactory,
+ new SourcePreloadControl(),
+ trackSelector,
+ bandwidthMeter,
+ rendererCapabilitiesList.getRendererCapabilities(),
+ allocator,
+ preloadLooper);
+ }
+
+ /**
+ * Sets the index of the current playing media.
+ *
+ * @param currentPlayingIndex The index of current playing media.
+ */
+ public void setCurrentPlayingIndex(int currentPlayingIndex) {
+ RankingDataComparator rankingDataComparator =
+ (RankingDataComparator) this.rankingDataComparator;
+ rankingDataComparator.currentPlayingIndex = currentPlayingIndex;
+ }
+
+ @Override
+ public MediaSource createMediaSourceForPreloading(MediaSource mediaSource) {
+ return preloadMediaSourceFactory.createMediaSource(mediaSource);
+ }
+
+ @Override
+ protected void preloadSourceInternal(MediaSource mediaSource, long startPositionsUs) {
+ checkArgument(mediaSource instanceof PreloadMediaSource);
+ PreloadMediaSource preloadMediaSource = (PreloadMediaSource) mediaSource;
+ if (preloadMediaSource.isUsedByPlayer()) {
+ onPreloadCompleted(preloadMediaSource);
+ return;
+ }
+ preloadMediaSource.preload(startPositionsUs);
+ }
+
+ @Override
+ protected void releaseSourceInternal(MediaSource mediaSource) {
+ checkArgument(mediaSource instanceof PreloadMediaSource);
+ PreloadMediaSource preloadMediaSource = (PreloadMediaSource) mediaSource;
+ preloadMediaSource.releasePreloadMediaSource();
+ }
+
+ @Override
+ protected void releaseInternal() {
+ rendererCapabilitiesList.release();
+ }
+
+ private static final class RankingDataComparator implements Comparator {
+
+ public int currentPlayingIndex;
+
+ public RankingDataComparator() {
+ this.currentPlayingIndex = C.INDEX_UNSET;
+ }
+
+ @Override
+ public int compare(Integer o1, Integer o2) {
+ return Integer.compare(abs(o1 - currentPlayingIndex), abs(o2 - currentPlayingIndex));
+ }
+ }
+
+ private final class SourcePreloadControl implements PreloadMediaSource.PreloadControl {
+ @Override
+ public boolean onTimelineRefreshed(PreloadMediaSource mediaSource) {
+ return continueOrCompletePreloading(
+ mediaSource, status -> status.getStage() > Status.STAGE_TIMELINE_REFRESHED);
+ }
+
+ @Override
+ public boolean onPrepared(PreloadMediaSource mediaSource) {
+ return continueOrCompletePreloading(
+ mediaSource, status -> status.getStage() > Status.STAGE_SOURCE_PREPARED);
+ }
+
+ @Override
+ public boolean onContinueLoadingRequested(
+ PreloadMediaSource mediaSource, long bufferedPositionUs) {
+ return continueOrCompletePreloading(
+ mediaSource,
+ status ->
+ (status.getStage() == Status.STAGE_LOADED_TO_POSITION_MS
+ && status.getValue() > Util.usToMs(bufferedPositionUs)));
+ }
+
+ private boolean continueOrCompletePreloading(
+ MediaSource mediaSource, Predicate continueLoadingPredicate) {
+ @Nullable
+ TargetPreloadStatusControl.PreloadStatus targetPreloadStatus =
+ getTargetPreloadStatus(mediaSource);
+ checkState(targetPreloadStatus instanceof Status);
+ Status status = (Status) targetPreloadStatus;
+ if (continueLoadingPredicate.apply(checkNotNull(status))) {
+ return true;
+ }
+ onPreloadCompleted(mediaSource);
+ return false;
+ }
+ }
+}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/TargetPreloadStatusControl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/TargetPreloadStatusControl.java
new file mode 100644
index 0000000000..01e441fcea
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/TargetPreloadStatusControl.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media3.exoplayer.source.preload;
+
+import androidx.media3.common.util.UnstableApi;
+
+/** Controls the target preload status. */
+@UnstableApi
+public interface TargetPreloadStatusControl {
+
+ /** Returns the target preload status for a source with the given {@code rankingData}. */
+ PreloadStatus getTargetPreloadStatus(T rankingData);
+
+ /** Defines the status of the preloading for a source. */
+ interface PreloadStatus {
+
+ /** The stage of the preloading. */
+ int getStage();
+
+ /** The associated value of the preloading stage. */
+ long getValue();
+ }
+}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/DefaultRendererCapabilitiesListTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/DefaultRendererCapabilitiesListTest.java
new file mode 100644
index 0000000000..6485f64ff5
--- /dev/null
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/DefaultRendererCapabilitiesListTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media3.exoplayer;
+
+import static androidx.media3.common.util.Assertions.checkNotNull;
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.media3.common.util.SystemClock;
+import androidx.media3.test.utils.FakeAudioRenderer;
+import androidx.media3.test.utils.FakeRenderer;
+import androidx.media3.test.utils.FakeVideoRenderer;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link DefaultRendererCapabilitiesList}. */
+@RunWith(AndroidJUnit4.class)
+public class DefaultRendererCapabilitiesListTest {
+
+ private AtomicReference> underlyingRenderersReference;
+ private RenderersFactory renderersFactory;
+
+ @Before
+ public void setUp() {
+ underlyingRenderersReference = new AtomicReference<>();
+ renderersFactory =
+ (eventHandler,
+ videoRendererEventListener,
+ audioRendererEventListener,
+ textRendererOutput,
+ metadataRendererOutput) -> {
+ FakeRenderer[] createdRenderers =
+ new FakeRenderer[] {
+ new FakeVideoRenderer(
+ SystemClock.DEFAULT.createHandler(
+ eventHandler.getLooper(), /* callback= */ null),
+ videoRendererEventListener),
+ new FakeAudioRenderer(
+ SystemClock.DEFAULT.createHandler(
+ eventHandler.getLooper(), /* callback= */ null),
+ audioRendererEventListener)
+ };
+ underlyingRenderersReference.set(ImmutableList.copyOf(createdRenderers));
+ return createdRenderers;
+ };
+ }
+
+ @Test
+ public void createRendererCapabilitiesList_underlyingRenderersInitialized() {
+ DefaultRendererCapabilitiesList.Factory rendererCapabilitiesFactory =
+ new DefaultRendererCapabilitiesList.Factory(renderersFactory);
+
+ rendererCapabilitiesFactory.createRendererCapabilitiesList();
+
+ List underlyingRenderers = checkNotNull(underlyingRenderersReference.get());
+ for (FakeRenderer renderer : underlyingRenderers) {
+ assertThat(renderer.isInitialized).isTrue();
+ }
+ }
+
+ @Test
+ public void getRendererCapabilities_returnsExpectedRendererCapabilities() {
+ DefaultRendererCapabilitiesList.Factory rendererCapabilitiesFactory =
+ new DefaultRendererCapabilitiesList.Factory(renderersFactory);
+ DefaultRendererCapabilitiesList rendererCapabilitiesList =
+ rendererCapabilitiesFactory.createRendererCapabilitiesList();
+
+ RendererCapabilities[] rendererCapabilities =
+ rendererCapabilitiesList.getRendererCapabilities();
+
+ List underlyingRenderers = checkNotNull(underlyingRenderersReference.get());
+ assertThat(rendererCapabilities).hasLength(underlyingRenderers.size());
+ for (int i = 0; i < rendererCapabilities.length; i++) {
+ assertThat(rendererCapabilities[i].getTrackType())
+ .isEqualTo(underlyingRenderers.get(i).getTrackType());
+ }
+ }
+
+ @Test
+ public void release_underlyingRenderersReleased() {
+ DefaultRendererCapabilitiesList.Factory rendererCapabilitiesFactory =
+ new DefaultRendererCapabilitiesList.Factory(renderersFactory);
+ DefaultRendererCapabilitiesList rendererCapabilitiesList =
+ rendererCapabilitiesFactory.createRendererCapabilitiesList();
+
+ rendererCapabilitiesList.release();
+
+ List underlyingRenderers = checkNotNull(underlyingRenderersReference.get());
+ for (FakeRenderer renderer : underlyingRenderers) {
+ assertThat(renderer.isReleased).isTrue();
+ }
+ }
+}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/DefaultPreloadManagerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/DefaultPreloadManagerTest.java
new file mode 100644
index 0000000000..3320ab8ac7
--- /dev/null
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/DefaultPreloadManagerTest.java
@@ -0,0 +1,455 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media3.exoplayer.source.preload;
+
+import static androidx.media3.common.util.Assertions.checkNotNull;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Context;
+import android.os.Looper;
+import androidx.annotation.Nullable;
+import androidx.media3.common.C;
+import androidx.media3.common.MediaItem;
+import androidx.media3.common.util.SystemClock;
+import androidx.media3.common.util.Util;
+import androidx.media3.exoplayer.DefaultRendererCapabilitiesList;
+import androidx.media3.exoplayer.Renderer;
+import androidx.media3.exoplayer.RendererCapabilitiesList;
+import androidx.media3.exoplayer.RenderersFactory;
+import androidx.media3.exoplayer.analytics.PlayerId;
+import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
+import androidx.media3.exoplayer.source.MediaSource;
+import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
+import androidx.media3.exoplayer.trackselection.TrackSelector;
+import androidx.media3.exoplayer.upstream.Allocator;
+import androidx.media3.exoplayer.upstream.BandwidthMeter;
+import androidx.media3.exoplayer.upstream.DefaultAllocator;
+import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter;
+import androidx.media3.test.utils.FakeAudioRenderer;
+import androidx.media3.test.utils.FakeMediaSource;
+import androidx.media3.test.utils.FakeMediaSourceFactory;
+import androidx.media3.test.utils.FakeRenderer;
+import androidx.media3.test.utils.FakeVideoRenderer;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+/** Unit test for {@link DefaultPreloadManager}. */
+@RunWith(AndroidJUnit4.class)
+public class DefaultPreloadManagerTest {
+ @Mock private TargetPreloadStatusControl mockTargetPreloadStatusControl;
+ private PlayerId playerId;
+ private TrackSelector trackSelector;
+ private Allocator allocator;
+ private BandwidthMeter bandwidthMeter;
+ private RendererCapabilitiesList.Factory rendererCapabilitiesListFactory;
+
+ @Before
+ public void setUp() {
+ playerId = new PlayerId();
+ trackSelector = new DefaultTrackSelector(ApplicationProvider.getApplicationContext());
+ allocator = new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
+ bandwidthMeter =
+ new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();
+ RenderersFactory renderersFactory =
+ (handler, videoListener, audioListener, textOutput, metadataOutput) ->
+ new Renderer[] {
+ new FakeVideoRenderer(
+ SystemClock.DEFAULT.createHandler(handler.getLooper(), /* callback= */ null),
+ videoListener),
+ new FakeAudioRenderer(
+ SystemClock.DEFAULT.createHandler(handler.getLooper(), /* callback= */ null),
+ audioListener)
+ };
+ rendererCapabilitiesListFactory = new DefaultRendererCapabilitiesList.Factory(renderersFactory);
+ trackSelector.init(/* listener= */ () -> {}, bandwidthMeter);
+ }
+
+ @Test
+ public void addByMediaItems_getCorrectCountAndSources() {
+ DefaultPreloadManager preloadManager =
+ new DefaultPreloadManager(
+ mockTargetPreloadStatusControl,
+ new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()),
+ trackSelector,
+ bandwidthMeter,
+ rendererCapabilitiesListFactory,
+ allocator,
+ Util.getCurrentOrMainLooper());
+ MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
+ MediaItem mediaItem1 =
+ mediaItemBuilder.setMediaId("mediaId1").setUri("http://exoplayer.dev/video1").build();
+ MediaItem mediaItem2 =
+ mediaItemBuilder.setMediaId("mediaId2").setUri("http://exoplayer.dev/video2").build();
+
+ preloadManager.add(mediaItem1, /* rankingData= */ 1);
+ preloadManager.add(mediaItem2, /* rankingData= */ 2);
+
+ assertThat(preloadManager.getSourceCount()).isEqualTo(2);
+ assertThat(preloadManager.getMediaSource(mediaItem1).getMediaItem()).isEqualTo(mediaItem1);
+ assertThat(preloadManager.getMediaSource(mediaItem2).getMediaItem()).isEqualTo(mediaItem2);
+ }
+
+ @Test
+ public void addByMediaSources_getCorrectCountAndSources() {
+ DefaultPreloadManager preloadManager =
+ new DefaultPreloadManager(
+ mockTargetPreloadStatusControl,
+ new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()),
+ trackSelector,
+ bandwidthMeter,
+ rendererCapabilitiesListFactory,
+ allocator,
+ Util.getCurrentOrMainLooper());
+ MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
+ MediaItem mediaItem1 =
+ mediaItemBuilder.setMediaId("mediaId1").setUri("http://exoplayer.dev/video1").build();
+ MediaItem mediaItem2 =
+ mediaItemBuilder.setMediaId("mediaId2").setUri("http://exoplayer.dev/video2").build();
+ DefaultMediaSourceFactory defaultMediaSourceFactory =
+ new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext());
+ MediaSource mediaSourceToAdd1 = defaultMediaSourceFactory.createMediaSource(mediaItem1);
+ MediaSource mediaSourceToAdd2 = defaultMediaSourceFactory.createMediaSource(mediaItem2);
+
+ preloadManager.add(mediaSourceToAdd1, /* rankingData= */ 1);
+ preloadManager.add(mediaSourceToAdd2, /* rankingData= */ 2);
+
+ assertThat(preloadManager.getSourceCount()).isEqualTo(2);
+ assertThat(preloadManager.getMediaSource(mediaItem1).getMediaItem()).isEqualTo(mediaItem1);
+ assertThat(preloadManager.getMediaSource(mediaItem2).getMediaItem()).isEqualTo(mediaItem2);
+ }
+
+ @Test
+ public void getMediaSourceForMediaItemNotAdded() {
+ DefaultPreloadManager preloadManager =
+ new DefaultPreloadManager(
+ mockTargetPreloadStatusControl,
+ new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()),
+ trackSelector,
+ bandwidthMeter,
+ rendererCapabilitiesListFactory,
+ allocator,
+ Util.getCurrentOrMainLooper());
+ MediaItem mediaItem =
+ new MediaItem.Builder()
+ .setMediaId("mediaId1")
+ .setUri("http://exoplayer.dev/video1")
+ .build();
+
+ @Nullable MediaSource mediaSource = preloadManager.getMediaSource(mediaItem);
+
+ assertThat(mediaSource).isNull();
+ }
+
+ @Test
+ public void
+ invalidate_withoutSettingCurrentPlayingIndex_sourcesPreloadedToTargetStatusesInOrder() {
+ ArrayList targetPreloadStatusControlCallReference = new ArrayList<>();
+ TargetPreloadStatusControl targetPreloadStatusControl =
+ rankingData -> {
+ targetPreloadStatusControlCallReference.add(rankingData);
+ return new DefaultPreloadManager.Status(
+ DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED);
+ };
+ FakeMediaSourceFactory fakeMediaSourceFactory = new FakeMediaSourceFactory();
+ DefaultPreloadManager preloadManager =
+ new DefaultPreloadManager(
+ targetPreloadStatusControl,
+ fakeMediaSourceFactory,
+ trackSelector,
+ bandwidthMeter,
+ rendererCapabilitiesListFactory,
+ allocator,
+ Util.getCurrentOrMainLooper());
+ MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
+ MediaItem mediaItem0 =
+ mediaItemBuilder.setMediaId("mediaId0").setUri("http://exoplayer.dev/video0").build();
+ MediaItem mediaItem1 =
+ mediaItemBuilder.setMediaId("mediaId1").setUri("http://exoplayer.dev/video1").build();
+ MediaItem mediaItem2 =
+ mediaItemBuilder.setMediaId("mediaId2").setUri("http://exoplayer.dev/video2").build();
+
+ preloadManager.add(mediaItem0, /* rankingData= */ 0);
+ FakeMediaSource wrappedMediaSource0 = fakeMediaSourceFactory.getLastCreatedSource();
+ wrappedMediaSource0.setAllowPreparation(false);
+ preloadManager.add(mediaItem1, /* rankingData= */ 1);
+ FakeMediaSource wrappedMediaSource1 = fakeMediaSourceFactory.getLastCreatedSource();
+ wrappedMediaSource1.setAllowPreparation(false);
+ preloadManager.add(mediaItem2, /* rankingData= */ 2);
+ FakeMediaSource wrappedMediaSource2 = fakeMediaSourceFactory.getLastCreatedSource();
+ wrappedMediaSource2.setAllowPreparation(false);
+ preloadManager.invalidate();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(targetPreloadStatusControlCallReference).containsExactly(0);
+ wrappedMediaSource0.setAllowPreparation(true);
+ shadowOf(Looper.getMainLooper()).idle();
+ assertThat(targetPreloadStatusControlCallReference).containsExactly(0, 1).inOrder();
+ }
+
+ @Test
+ public void invalidate_withSettingCurrentPlayingIndex_sourcesPreloadedToTargetStatusesInOrder() {
+ ArrayList targetPreloadStatusControlCallReference = new ArrayList<>();
+ TargetPreloadStatusControl targetPreloadStatusControl =
+ rankingData -> {
+ targetPreloadStatusControlCallReference.add(rankingData);
+ return new DefaultPreloadManager.Status(
+ DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED);
+ };
+ FakeMediaSourceFactory fakeMediaSourceFactory = new FakeMediaSourceFactory();
+ DefaultPreloadManager preloadManager =
+ new DefaultPreloadManager(
+ targetPreloadStatusControl,
+ fakeMediaSourceFactory,
+ trackSelector,
+ bandwidthMeter,
+ rendererCapabilitiesListFactory,
+ allocator,
+ Util.getCurrentOrMainLooper());
+ MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
+ MediaItem mediaItem0 =
+ mediaItemBuilder.setMediaId("mediaId0").setUri("http://exoplayer.dev/video0").build();
+ MediaItem mediaItem1 =
+ mediaItemBuilder.setMediaId("mediaId1").setUri("http://exoplayer.dev/video1").build();
+ MediaItem mediaItem2 =
+ mediaItemBuilder.setMediaId("mediaId2").setUri("http://exoplayer.dev/video2").build();
+
+ preloadManager.add(mediaItem0, /* rankingData= */ 0);
+ FakeMediaSource wrappedMediaSource0 = fakeMediaSourceFactory.getLastCreatedSource();
+ wrappedMediaSource0.setAllowPreparation(false);
+ preloadManager.add(mediaItem1, /* rankingData= */ 1);
+ FakeMediaSource wrappedMediaSource1 = fakeMediaSourceFactory.getLastCreatedSource();
+ wrappedMediaSource1.setAllowPreparation(false);
+ preloadManager.add(mediaItem2, /* rankingData= */ 2);
+ FakeMediaSource wrappedMediaSource2 = fakeMediaSourceFactory.getLastCreatedSource();
+ wrappedMediaSource2.setAllowPreparation(false);
+
+ MediaSource.MediaSourceCaller externalCaller = (source, timeline) -> {};
+ PreloadMediaSource preloadMediaSource2 =
+ (PreloadMediaSource) preloadManager.getMediaSource(mediaItem2);
+ preloadMediaSource2.prepareSource(
+ externalCaller, bandwidthMeter.getTransferListener(), playerId);
+ preloadManager.setCurrentPlayingIndex(2);
+ preloadManager.invalidate();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(targetPreloadStatusControlCallReference).containsExactly(2, 1);
+ wrappedMediaSource1.setAllowPreparation(true);
+ shadowOf(Looper.getMainLooper()).idle();
+ assertThat(targetPreloadStatusControlCallReference).containsExactly(2, 1, 0).inOrder();
+ }
+
+ @Test
+ public void invalidate_beforePreloadCompletedForLastInvalidate_preloadRespectsToLatestOrder() {
+ ArrayList targetPreloadStatusControlCallReference = new ArrayList<>();
+ TargetPreloadStatusControl targetPreloadStatusControl =
+ rankingData -> {
+ targetPreloadStatusControlCallReference.add(rankingData);
+ return new DefaultPreloadManager.Status(
+ DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED);
+ };
+ FakeMediaSourceFactory fakeMediaSourceFactory = new FakeMediaSourceFactory();
+ DefaultPreloadManager preloadManager =
+ new DefaultPreloadManager(
+ targetPreloadStatusControl,
+ fakeMediaSourceFactory,
+ trackSelector,
+ bandwidthMeter,
+ rendererCapabilitiesListFactory,
+ allocator,
+ Util.getCurrentOrMainLooper());
+ MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
+ MediaItem mediaItem0 =
+ mediaItemBuilder.setMediaId("mediaId0").setUri("http://exoplayer.dev/video0").build();
+ MediaItem mediaItem1 =
+ mediaItemBuilder.setMediaId("mediaId1").setUri("http://exoplayer.dev/video1").build();
+ MediaItem mediaItem2 =
+ mediaItemBuilder.setMediaId("mediaId2").setUri("http://exoplayer.dev/video2").build();
+ preloadManager.add(mediaItem0, /* rankingData= */ 0);
+ FakeMediaSource wrappedMediaSource0 = fakeMediaSourceFactory.getLastCreatedSource();
+ wrappedMediaSource0.setAllowPreparation(false);
+ preloadManager.add(mediaItem1, /* rankingData= */ 1);
+ FakeMediaSource wrappedMediaSource1 = fakeMediaSourceFactory.getLastCreatedSource();
+ wrappedMediaSource1.setAllowPreparation(false);
+ preloadManager.add(mediaItem2, /* rankingData= */ 2);
+ FakeMediaSource wrappedMediaSource2 = fakeMediaSourceFactory.getLastCreatedSource();
+ wrappedMediaSource2.setAllowPreparation(false);
+
+ MediaSource.MediaSourceCaller externalCaller = (source, timeline) -> {};
+ PreloadMediaSource preloadMediaSource0 =
+ (PreloadMediaSource) preloadManager.getMediaSource(mediaItem0);
+ preloadMediaSource0.prepareSource(
+ externalCaller, bandwidthMeter.getTransferListener(), playerId);
+ preloadManager.setCurrentPlayingIndex(0);
+ preloadManager.invalidate();
+ shadowOf(Looper.getMainLooper()).idle();
+ assertThat(targetPreloadStatusControlCallReference).containsExactly(0, 1).inOrder();
+ targetPreloadStatusControlCallReference.clear();
+
+ preloadMediaSource0.releaseSource(externalCaller);
+ PreloadMediaSource preloadMediaSource2 =
+ (PreloadMediaSource) preloadManager.getMediaSource(mediaItem2);
+ preloadMediaSource2.prepareSource(
+ externalCaller, bandwidthMeter.getTransferListener(), playerId);
+ preloadManager.setCurrentPlayingIndex(2);
+ preloadManager.invalidate();
+ shadowOf(Looper.getMainLooper()).idle();
+ assertThat(targetPreloadStatusControlCallReference).containsExactly(2, 1).inOrder();
+ wrappedMediaSource1.setAllowPreparation(true);
+ shadowOf(Looper.getMainLooper()).idle();
+ assertThat(targetPreloadStatusControlCallReference).containsExactly(2, 1, 0).inOrder();
+ }
+
+ @Test
+ public void removeMediaItemPreviouslyAdded_returnsCorrectCountAndNullSource_sourceReleased() {
+ TargetPreloadStatusControl targetPreloadStatusControl =
+ rankingData ->
+ new DefaultPreloadManager.Status(DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED);
+ MediaSource.Factory mockMediaSourceFactory = mock(MediaSource.Factory.class);
+ DefaultPreloadManager preloadManager =
+ new DefaultPreloadManager(
+ targetPreloadStatusControl,
+ mockMediaSourceFactory,
+ trackSelector,
+ bandwidthMeter,
+ rendererCapabilitiesListFactory,
+ allocator,
+ Util.getCurrentOrMainLooper());
+ MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
+ MediaItem mediaItem1 =
+ mediaItemBuilder.setMediaId("mediaId1").setUri("http://exoplayer.dev/video1").build();
+ MediaItem mediaItem2 =
+ mediaItemBuilder.setMediaId("mediaId2").setUri("http://exoplayer.dev/video2").build();
+ ArrayList internalSourceToReleaseReferenceByMediaId = new ArrayList<>();
+ when(mockMediaSourceFactory.createMediaSource(any()))
+ .thenAnswer(
+ invocation -> {
+ MediaItem mediaItem = invocation.getArgument(0);
+ return new FakeMediaSource() {
+ @Override
+ public MediaItem getMediaItem() {
+ return mediaItem;
+ }
+
+ @Override
+ protected void releaseSourceInternal() {
+ internalSourceToReleaseReferenceByMediaId.add(mediaItem.mediaId);
+ super.releaseSourceInternal();
+ }
+ };
+ });
+
+ preloadManager.add(mediaItem1, /* rankingData= */ 1);
+ preloadManager.add(mediaItem2, /* rankingData= */ 2);
+ preloadManager.invalidate();
+ shadowOf(Looper.getMainLooper()).idle();
+ preloadManager.remove(mediaItem1);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(preloadManager.getSourceCount()).isEqualTo(1);
+ assertThat(preloadManager.getMediaSource(mediaItem1)).isNull();
+ assertThat(preloadManager.getMediaSource(mediaItem2).getMediaItem()).isEqualTo(mediaItem2);
+ assertThat(internalSourceToReleaseReferenceByMediaId).containsExactly("mediaId1");
+ }
+
+ @Test
+ public void release_returnZeroCountAndNullSources_sourcesReleased() {
+ TargetPreloadStatusControl targetPreloadStatusControl =
+ rankingData ->
+ new DefaultPreloadManager.Status(DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED);
+ MediaSource.Factory mockMediaSourceFactory = mock(MediaSource.Factory.class);
+ AtomicReference> underlyingRenderersReference = new AtomicReference<>();
+ RenderersFactory renderersFactory =
+ (eventHandler,
+ videoRendererEventListener,
+ audioRendererEventListener,
+ textRendererOutput,
+ metadataRendererOutput) -> {
+ FakeRenderer[] createdRenderers =
+ new FakeRenderer[] {
+ new FakeVideoRenderer(
+ SystemClock.DEFAULT.createHandler(
+ eventHandler.getLooper(), /* callback= */ null),
+ videoRendererEventListener),
+ new FakeAudioRenderer(
+ SystemClock.DEFAULT.createHandler(
+ eventHandler.getLooper(), /* callback= */ null),
+ audioRendererEventListener)
+ };
+ underlyingRenderersReference.set(ImmutableList.copyOf(createdRenderers));
+ return createdRenderers;
+ };
+ DefaultPreloadManager preloadManager =
+ new DefaultPreloadManager(
+ targetPreloadStatusControl,
+ mockMediaSourceFactory,
+ trackSelector,
+ bandwidthMeter,
+ new DefaultRendererCapabilitiesList.Factory(renderersFactory),
+ allocator,
+ Util.getCurrentOrMainLooper());
+ MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
+ MediaItem mediaItem1 =
+ mediaItemBuilder.setMediaId("mediaId1").setUri("http://exoplayer.dev/video1").build();
+ MediaItem mediaItem2 =
+ mediaItemBuilder.setMediaId("mediaId2").setUri("http://exoplayer.dev/video2").build();
+ ArrayList internalSourceToReleaseReferenceByMediaId = new ArrayList<>();
+ when(mockMediaSourceFactory.createMediaSource(any()))
+ .thenAnswer(
+ invocation -> {
+ MediaItem mediaItem = invocation.getArgument(0);
+ return new FakeMediaSource() {
+ @Override
+ public MediaItem getMediaItem() {
+ return mediaItem;
+ }
+
+ @Override
+ protected void releaseSourceInternal() {
+ internalSourceToReleaseReferenceByMediaId.add(mediaItem.mediaId);
+ super.releaseSourceInternal();
+ }
+ };
+ });
+
+ preloadManager.add(mediaItem1, /* rankingData= */ 1);
+ preloadManager.add(mediaItem2, /* rankingData= */ 2);
+ preloadManager.invalidate();
+ shadowOf(Looper.getMainLooper()).idle();
+ preloadManager.release();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(preloadManager.getSourceCount()).isEqualTo(0);
+ assertThat(preloadManager.getMediaSource(mediaItem1)).isNull();
+ assertThat(preloadManager.getMediaSource(mediaItem2)).isNull();
+ assertThat(internalSourceToReleaseReferenceByMediaId).containsExactly("mediaId1", "mediaId2");
+ List underlyingRenderers = checkNotNull(underlyingRenderersReference.get());
+ for (FakeRenderer renderer : underlyingRenderers) {
+ assertThat(renderer.isReleased).isTrue();
+ }
+ }
+}