From 5e31cd9df34e770cd1301d2202460fc383b29799 Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Tue, 5 Mar 2024 07:11:00 -0800 Subject: [PATCH] Add BasePreloadManager and DefaultPreloadManager `BasePreloadManager` coordinates the preloading for multiple sources based on the priorities defined by their `rankingData`. Customization is possible by extending this class. Apps will implement `TargetPreloadStatusControl` to return preload manager the target preload status for a given `rankingData` of the source. `DefaultPreloadManager` extends from the above base class and uses `PreloadMediaSource` to preload media samples of the sources into memory. It also uses an integer `rankingData` that indicates the index of an item on the UI, and the priority of the items is determined by their adjacency to the current playing item. Apps can set the index of current playing item via `DefaultPreloadManager.setCurrentPlayingIndex` when the user swiping is detected. PiperOrigin-RevId: 612829642 --- RELEASENOTES.md | 6 + .../DefaultRendererCapabilitiesList.java | 93 ++++ .../exoplayer/RendererCapabilitiesList.java | 36 ++ .../source/preload/BasePreloadManager.java | 272 +++++++++++ .../source/preload/DefaultPreloadManager.java | 244 ++++++++++ .../preload/TargetPreloadStatusControl.java | 36 ++ .../DefaultRendererCapabilitiesListTest.java | 110 +++++ .../preload/DefaultPreloadManagerTest.java | 455 ++++++++++++++++++ 8 files changed, 1252 insertions(+) create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRendererCapabilitiesList.java create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilitiesList.java create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/BasePreloadManager.java create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/DefaultPreloadManager.java create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/TargetPreloadStatusControl.java create mode 100644 libraries/exoplayer/src/test/java/androidx/media3/exoplayer/DefaultRendererCapabilitiesListTest.java create mode 100644 libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/DefaultPreloadManagerTest.java 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(); + } + } +}