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
This commit is contained in:
tianyifeng 2024-03-05 07:11:00 -08:00 committed by Copybara-Service
parent 3a43bd7687
commit 5e31cd9df3
8 changed files with 1252 additions and 0 deletions

View file

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

View file

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

View file

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

View file

@ -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}.
*
* <p>Methods should be called on the same thread.
*/
@UnstableApi
public abstract class BasePreloadManager<T> {
/** A base class of the builder of the concrete extension of {@link BasePreloadManager}. */
protected abstract static class BuilderBase<T> {
protected final Comparator<T> rankingDataComparator;
protected final TargetPreloadStatusControl<T> targetPreloadStatusControl;
protected final MediaSource.Factory mediaSourceFactory;
public BuilderBase(
Comparator<T> rankingDataComparator,
TargetPreloadStatusControl<T> targetPreloadStatusControl,
MediaSource.Factory mediaSourceFactory) {
this.rankingDataComparator = rankingDataComparator;
this.targetPreloadStatusControl = targetPreloadStatusControl;
this.mediaSourceFactory = mediaSourceFactory;
}
public abstract BasePreloadManager<T> build();
}
private final Object lock;
protected final Comparator<T> rankingDataComparator;
private final TargetPreloadStatusControl<T> targetPreloadStatusControl;
private final MediaSource.Factory mediaSourceFactory;
private final Map<MediaItem, MediaSourceHolder> mediaItemMediaSourceHolderMap;
private final Handler startPreloadingHandler;
@GuardedBy("lock")
private final PriorityQueue<MediaSourceHolder> sourceHolderPriorityQueue;
@GuardedBy("lock")
@Nullable
private TargetPreloadStatusControl.PreloadStatus targetPreloadStatusOfCurrentPreloadingSource;
protected BasePreloadManager(
Comparator<T> rankingDataComparator,
TargetPreloadStatusControl<T> 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<MediaSourceHolder> {
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<T>.MediaSourceHolder o) {
return rankingDataComparator.compare(this.rankingData, o.rankingData);
}
}
}

View file

@ -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<Integer> {
/**
* 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<Integer> 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<Integer> {
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<Status> 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;
}
}
}

View file

@ -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<T> {
/** 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();
}
}

View file

@ -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<List<FakeRenderer>> 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<FakeRenderer> 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<FakeRenderer> 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<FakeRenderer> underlyingRenderers = checkNotNull(underlyingRenderersReference.get());
for (FakeRenderer renderer : underlyingRenderers) {
assertThat(renderer.isReleased).isTrue();
}
}
}

View file

@ -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<Integer> 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<Integer> targetPreloadStatusControlCallReference = new ArrayList<>();
TargetPreloadStatusControl<Integer> 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<Integer> targetPreloadStatusControlCallReference = new ArrayList<>();
TargetPreloadStatusControl<Integer> 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<Integer> targetPreloadStatusControlCallReference = new ArrayList<>();
TargetPreloadStatusControl<Integer> 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<Integer> 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<String> 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<Integer> targetPreloadStatusControl =
rankingData ->
new DefaultPreloadManager.Status(DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED);
MediaSource.Factory mockMediaSourceFactory = mock(MediaSource.Factory.class);
AtomicReference<List<FakeRenderer>> 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<String> 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<FakeRenderer> underlyingRenderers = checkNotNull(underlyingRenderersReference.get());
for (FakeRenderer renderer : underlyingRenderers) {
assertThat(renderer.isReleased).isTrue();
}
}
}