Add Listener to BasePreloadManager to propagate preload events to apps

PiperOrigin-RevId: 651421044
This commit is contained in:
tianyifeng 2024-07-11 08:34:44 -07:00 committed by Copybara-Service
parent 735e0cf8a1
commit 7aa70a5f2f
4 changed files with 225 additions and 28 deletions

View File

@ -21,6 +21,7 @@
error occurs.
* `MediaCodecVideoRenderer` avoids decoding samples that are neither
rendered nor used as reference by other samples.
* Add `BasePreloadManager.Listener` to propagate preload events to apps.
* Transformer:
* Add `SurfaceAssetLoader`, which supports queueing video data to
Transformer via a `Surface`.

View File

@ -18,10 +18,13 @@ package androidx.media3.exoplayer.source.preload;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.source.MediaSource;
@ -58,10 +61,22 @@ public abstract class BasePreloadManager<T> {
public abstract BasePreloadManager<T> build();
}
/** Listener for events in a preload manager. */
public interface Listener {
/** Called when the given {@link MediaItem} has completed preloading. */
void onCompleted(MediaItem mediaItem);
/** Called when an {@linkplain PreloadException error} occurs. */
void onError(PreloadException exception);
}
private final Object lock;
private final Looper looper;
protected final Comparator<T> rankingDataComparator;
private final TargetPreloadStatusControl<T> targetPreloadStatusControl;
private final MediaSource.Factory mediaSourceFactory;
private final ListenerSet<Listener> listeners;
private final Map<MediaItem, MediaSourceHolder> mediaItemMediaSourceHolderMap;
private final Handler startPreloadingHandler;
@ -77,14 +92,45 @@ public abstract class BasePreloadManager<T> {
TargetPreloadStatusControl<T> targetPreloadStatusControl,
MediaSource.Factory mediaSourceFactory) {
lock = new Object();
looper = Util.getCurrentOrMainLooper();
this.rankingDataComparator = rankingDataComparator;
this.targetPreloadStatusControl = targetPreloadStatusControl;
this.mediaSourceFactory = mediaSourceFactory;
listeners = new ListenerSet<>(looper, Clock.DEFAULT, (listener, flags) -> {});
mediaItemMediaSourceHolderMap = new HashMap<>();
startPreloadingHandler = Util.createHandlerForCurrentOrMainLooper();
sourceHolderPriorityQueue = new PriorityQueue<>();
}
/**
* Adds a {@link Listener} to listen to the preload events.
*
* <p>This method can be called from any thread.
*/
public void addListener(Listener listener) {
listeners.add(listener);
}
/**
* Removes a {@link Listener}.
*
* @throws IllegalStateException If this method is called from the wrong thread.
*/
public void removeListener(Listener listener) {
verifyApplicationThread();
listeners.remove(listener);
}
/**
* Clears all the {@linkplain Listener listeners}.
*
* @throws IllegalStateException If this method is called from the wrong thread.
*/
public void clearListeners() {
verifyApplicationThread();
listeners.clear();
}
/**
* Gets the count of the {@linkplain MediaSource media sources} currently being managed by the
* preload manager.
@ -206,15 +252,33 @@ public abstract class BasePreloadManager<T> {
public final void release() {
reset();
releaseInternal();
clearListeners();
}
/** Called when the given {@link MediaSource} completes to preload. */
/** Called when the given {@link MediaSource} completes preloading. */
protected final void onPreloadCompleted(MediaSource source) {
listeners.sendEvent(
/* eventFlag= */ C.INDEX_UNSET, listener -> listener.onCompleted(source.getMediaItem()));
maybeAdvanceToNextSource(source);
}
/** Called when an error occurs. */
protected final void onPreloadError(PreloadException error, MediaSource source) {
listeners.sendEvent(/* eventFlag= */ C.INDEX_UNSET, listener -> listener.onError(error));
maybeAdvanceToNextSource(source);
}
/** Called when the given {@link MediaSource} has been skipped before completing preloading. */
protected final void onPreloadSkipped(MediaSource source) {
maybeAdvanceToNextSource(source);
}
private void maybeAdvanceToNextSource(MediaSource preloadingSource) {
startPreloadingHandler.post(
() -> {
synchronized (lock) {
if (sourceHolderPriorityQueue.isEmpty()
|| checkNotNull(sourceHolderPriorityQueue.peek()).mediaSource != source) {
|| checkNotNull(sourceHolderPriorityQueue.peek()).mediaSource != preloadingSource) {
return;
}
do {
@ -307,6 +371,12 @@ public abstract class BasePreloadManager<T> {
return false;
}
private void verifyApplicationThread() {
if (Looper.myLooper() != looper) {
throw new IllegalStateException("Preload manager is accessed on the wrong thread.");
}
}
/** A holder for information for preloading a single media source. */
private final class MediaSourceHolder implements Comparable<MediaSourceHolder> {

View File

@ -241,17 +241,17 @@ public final class DefaultPreloadManager extends BasePreloadManager<Integer> {
@Override
public void onUsedByPlayer(PreloadMediaSource mediaSource) {
onPreloadCompleted(mediaSource);
DefaultPreloadManager.this.onPreloadSkipped(mediaSource);
}
@Override
public void onLoadedToTheEndOfSource(PreloadMediaSource mediaSource) {
onPreloadCompleted(mediaSource);
DefaultPreloadManager.this.onPreloadCompleted(mediaSource);
}
@Override
public void onPreloadError(PreloadException error, PreloadMediaSource mediaSource) {
onPreloadCompleted(mediaSource);
DefaultPreloadManager.this.onPreloadError(error, mediaSource);
}
private boolean continueOrCompletePreloading(
@ -269,8 +269,10 @@ public final class DefaultPreloadManager extends BasePreloadManager<Integer> {
if (clearExceededDataFromTargetPreloadStatus) {
clearSourceInternal(mediaSource);
}
DefaultPreloadManager.this.onPreloadCompleted(mediaSource);
} else {
DefaultPreloadManager.this.onPreloadSkipped(mediaSource);
}
onPreloadCompleted(mediaSource);
return false;
}
}

View File

@ -45,6 +45,7 @@ import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource;
@ -57,6 +58,7 @@ 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.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.test.utils.FakeAudioRenderer;
import androidx.media3.test.utils.FakeMediaPeriod;
import androidx.media3.test.utils.FakeMediaSource;
@ -67,6 +69,8 @@ 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 com.google.common.collect.Iterables;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@ -207,6 +211,8 @@ public class DefaultPreloadManagerTest {
rendererCapabilitiesListFactory,
allocator,
Util.getCurrentOrMainLooper());
TestPreloadManagerListener preloadManagerListener = new TestPreloadManagerListener();
preloadManager.addListener(preloadManagerListener);
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
MediaItem mediaItem0 =
mediaItemBuilder
@ -228,9 +234,12 @@ public class DefaultPreloadManagerTest {
preloadManager.add(mediaItem2, /* rankingData= */ 2);
preloadManager.invalidate();
runMainLooperUntil(() -> targetPreloadStatusControlCallStates.size() == 3);
runMainLooperUntil(() -> preloadManagerListener.onCompletedMediaItemRecords.size() == 3);
assertThat(targetPreloadStatusControlCallStates).containsExactly(0, 1, 2).inOrder();
assertThat(preloadManagerListener.onCompletedMediaItemRecords)
.containsExactly(mediaItem0, mediaItem1, mediaItem2)
.inOrder();
}
@Test
@ -259,6 +268,8 @@ public class DefaultPreloadManagerTest {
rendererCapabilitiesListFactory,
allocator,
Util.getCurrentOrMainLooper());
TestPreloadManagerListener preloadManagerListener = new TestPreloadManagerListener();
preloadManager.addListener(preloadManagerListener);
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
MediaItem mediaItem0 =
mediaItemBuilder
@ -278,17 +289,16 @@ public class DefaultPreloadManagerTest {
preloadManager.add(mediaItem0, /* rankingData= */ 0);
preloadManager.add(mediaItem1, /* rankingData= */ 1);
preloadManager.add(mediaItem2, /* rankingData= */ 2);
PreloadMediaSource preloadMediaSource2 =
(PreloadMediaSource) preloadManager.getMediaSource(mediaItem2);
preloadMediaSource2.prepareSource(
(source, timeline) -> {}, bandwidthMeter.getTransferListener(), PlayerId.UNSET);
preloadManager.setCurrentPlayingIndex(2);
currentPlayingItemIndex.set(2);
preloadManager.invalidate();
runMainLooperUntil(() -> targetPreloadStatusControlCallStates.size() == 3);
runMainLooperUntil(() -> preloadManagerListener.onCompletedMediaItemRecords.size() == 3);
assertThat(targetPreloadStatusControlCallStates).containsExactly(2, 1, 0).inOrder();
assertThat(preloadManagerListener.onCompletedMediaItemRecords)
.containsExactly(mediaItem2, mediaItem1, mediaItem0)
.inOrder();
}
@Test
@ -309,6 +319,8 @@ public class DefaultPreloadManagerTest {
rendererCapabilitiesListFactory,
allocator,
Util.getCurrentOrMainLooper());
TestPreloadManagerListener preloadManagerListener = new TestPreloadManagerListener();
preloadManager.addListener(preloadManagerListener);
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
MediaItem mediaItem0 =
mediaItemBuilder.setMediaId("mediaId0").setUri("http://exoplayer.dev/video0").build();
@ -327,11 +339,12 @@ public class DefaultPreloadManagerTest {
(PreloadMediaSource) preloadManager.getMediaSource(mediaItem0);
preloadMediaSource0.prepareSource(
(source, timeline) -> {}, bandwidthMeter.getTransferListener(), PlayerId.UNSET);
wrappedMediaSource0.setAllowPreparation(true);
wrappedMediaSource1.setAllowPreparation(true);
shadowOf(Looper.getMainLooper()).idle();
// The preload of mediaItem0 should complete and the preload manager continues to preload
// mediaItem1, even when the preloadMediaSource0 hasn't finished preparation.
assertThat(targetPreloadStatusControlCallStates).containsExactly(0, 1).inOrder();
assertThat(preloadManagerListener.onCompletedMediaItemRecords).containsExactly(mediaItem1);
}
@Test
@ -352,6 +365,8 @@ public class DefaultPreloadManagerTest {
rendererCapabilitiesListFactory,
allocator,
Util.getCurrentOrMainLooper());
TestPreloadManagerListener preloadManagerListener = new TestPreloadManagerListener();
preloadManager.addListener(preloadManagerListener);
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
MediaItem mediaItem0 =
mediaItemBuilder.setMediaId("mediaId0").setUri("http://exoplayer.dev/video0").build();
@ -368,35 +383,31 @@ public class DefaultPreloadManagerTest {
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.UNSET);
preloadManager.setCurrentPlayingIndex(0);
preloadManager.invalidate();
wrappedMediaSource0.setAllowPreparation(true);
shadowOf(Looper.getMainLooper()).idle();
assertThat(targetPreloadStatusControlCallStates).containsExactly(0, 1).inOrder();
assertThat(preloadManagerListener.onCompletedMediaItemRecords).containsExactly(mediaItem0);
targetPreloadStatusControlCallStates.clear();
preloadMediaSource0.releaseSource(externalCaller);
PreloadMediaSource preloadMediaSource2 =
(PreloadMediaSource) preloadManager.getMediaSource(mediaItem2);
preloadMediaSource2.prepareSource(
externalCaller, bandwidthMeter.getTransferListener(), PlayerId.UNSET);
preloadManagerListener.reset();
preloadManager.setCurrentPlayingIndex(2);
preloadManager.invalidate();
// Simulate the delay of the preparation of wrappedMediaSource0, which was triggered at the
// Simulate the delay of the preparation of wrappedMediaSource1, which was triggered at the
// first call of invalidate(). This is expected to result in nothing, as the whole flow of
// preloading should respect the priority order triggered by the latest call of invalidate().
wrappedMediaSource0.setAllowPreparation(true);
shadowOf(Looper.getMainLooper()).idle();
assertThat(targetPreloadStatusControlCallStates).containsExactly(2, 1).inOrder();
wrappedMediaSource1.setAllowPreparation(true);
shadowOf(Looper.getMainLooper()).idle();
assertThat(preloadManagerListener.onCompletedMediaItemRecords).isEmpty();
wrappedMediaSource2.setAllowPreparation(true);
shadowOf(Looper.getMainLooper()).idle();
assertThat(targetPreloadStatusControlCallStates).containsExactly(2, 1, 0).inOrder();
assertThat(preloadManagerListener.onCompletedMediaItemRecords)
.containsExactly(mediaItem2, mediaItem1, mediaItem0)
.inOrder();
}
@Test
@ -419,6 +430,8 @@ public class DefaultPreloadManagerTest {
rendererCapabilitiesListFactory,
allocator,
Util.getCurrentOrMainLooper());
TestPreloadManagerListener preloadManagerListener = new TestPreloadManagerListener();
preloadManager.addListener(preloadManagerListener);
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
MediaItem mediaItem0 =
mediaItemBuilder
@ -443,6 +456,91 @@ public class DefaultPreloadManagerTest {
shadowOf(Looper.getMainLooper()).idle();
assertThat(targetPreloadStatusControlCallStates).containsExactly(0, 1, 2);
assertThat(preloadManagerListener.onCompletedMediaItemRecords).isEmpty();
}
@Test
public void invalidate_sourceHasPreloadException_continuesPreloadingNextSource() {
ArrayList<Integer> targetPreloadStatusControlCallStates = new ArrayList<>();
TargetPreloadStatusControl<Integer> targetPreloadStatusControl =
rankingData -> {
targetPreloadStatusControlCallStates.add(rankingData);
return new DefaultPreloadManager.Status(STAGE_SOURCE_PREPARED);
};
IOException causeException = new IOException("Failed to refresh source info");
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();
MediaSource.Factory mediaSourceFactory =
new MediaSource.Factory() {
@Override
public MediaSource.Factory setDrmSessionManagerProvider(
DrmSessionManagerProvider drmSessionManagerProvider) {
return this;
}
@Override
public MediaSource.Factory setLoadErrorHandlingPolicy(
LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
return this;
}
@Override
public @C.ContentType int[] getSupportedTypes() {
return new int[0];
}
@Override
public MediaSource createMediaSource(MediaItem mediaItem) {
FakeMediaSource mediaSource =
new FakeMediaSource() {
@Override
public MediaItem getMediaItem() {
return mediaItem;
}
};
if (mediaItem.equals(mediaItem0)) {
mediaSource =
new FakeMediaSource() {
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
throw causeException;
}
@Override
public MediaItem getMediaItem() {
return mediaItem;
}
};
mediaSource.setAllowPreparation(false);
}
return mediaSource;
}
};
DefaultPreloadManager preloadManager =
new DefaultPreloadManager(
targetPreloadStatusControl,
mediaSourceFactory,
trackSelector,
bandwidthMeter,
rendererCapabilitiesListFactory,
allocator,
Util.getCurrentOrMainLooper());
TestPreloadManagerListener preloadManagerListener = new TestPreloadManagerListener();
preloadManager.addListener(preloadManagerListener);
preloadManager.add(mediaItem0, /* rankingData= */ 0);
preloadManager.add(mediaItem1, /* rankingData= */ 1);
preloadManager.invalidate();
shadowOf(Looper.getMainLooper()).idle();
assertThat(targetPreloadStatusControlCallStates).containsExactly(0, 1).inOrder();
assertThat(Iterables.getOnlyElement(preloadManagerListener.onErrorPreloadExceptionRecords))
.hasCauseThat()
.isEqualTo(causeException);
assertThat(preloadManagerListener.onCompletedMediaItemRecords).containsExactly(mediaItem1);
}
@Test
@ -820,4 +918,30 @@ public class DefaultPreloadManagerTest {
assertThat(renderer.isReleased).isTrue();
}
}
private static class TestPreloadManagerListener implements BasePreloadManager.Listener {
public final List<MediaItem> onCompletedMediaItemRecords;
public final List<PreloadException> onErrorPreloadExceptionRecords;
public TestPreloadManagerListener() {
onCompletedMediaItemRecords = new ArrayList<>();
onErrorPreloadExceptionRecords = new ArrayList<>();
}
@Override
public void onCompleted(MediaItem mediaItem) {
onCompletedMediaItemRecords.add(mediaItem);
}
@Override
public void onError(PreloadException exception) {
onErrorPreloadExceptionRecords.add(exception);
}
public void reset() {
onCompletedMediaItemRecords.clear();
onErrorPreloadExceptionRecords.clear();
}
}
}