diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fc3bddb03e..28d27c1b7a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -30,6 +30,9 @@ should be shown. Custom `SimpleDecoder` implementations can check `isAtLeastOutputStartTimeUs` if needed or mark other buffers with `DecoderOutputBuffer.shouldBeSkipped` to skip them. + * Allow a null value to be returned by + `TargetPreloadStatusControl.getTargetPreloadStatus(T)` to indicate not + to preload a `MediaSource` with the given `rankingData`. * 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/source/preload/BasePreloadManager.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/BasePreloadManager.java index 10bfba34f4..5e89c2545f 100644 --- 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 @@ -126,7 +126,9 @@ public abstract class BasePreloadManager { synchronized (lock) { sourceHolderPriorityQueue.clear(); sourceHolderPriorityQueue.addAll(mediaItemMediaSourceHolderMap.values()); - maybeStartPreloadNextSource(); + while (!sourceHolderPriorityQueue.isEmpty() && !maybeStartPreloadNextSource()) { + sourceHolderPriorityQueue.poll(); + } } } @@ -180,8 +182,9 @@ public abstract class BasePreloadManager { || checkNotNull(sourceHolderPriorityQueue.peek()).mediaSource != source) { return; } - sourceHolderPriorityQueue.poll(); - maybeStartPreloadNextSource(); + do { + sourceHolderPriorityQueue.poll(); + } while (!sourceHolderPriorityQueue.isEmpty() && !maybeStartPreloadNextSource()); } }); } @@ -237,14 +240,26 @@ public abstract class BasePreloadManager { /** Releases the preload manager, see {@link #release()}. */ protected void releaseInternal() {} + /** + * Starts to preload the {@link MediaSource} at the head of the priority queue, if the {@linkplain + * TargetPreloadStatusControl.PreloadStatus target preload status} for that source is not null. + * + * @return {@code true} if the {@link MediaSource} at the head of the priority queue starts to + * preload, otherwise {@code false}. + * @throws NullPointerException if the priority queue is empty. + */ @GuardedBy("lock") - private void maybeStartPreloadNextSource() { - if (!sourceHolderPriorityQueue.isEmpty() && shouldStartPreloadingNextSource()) { + private boolean maybeStartPreloadNextSource() { + if (shouldStartPreloadingNextSource()) { MediaSourceHolder preloadingHolder = checkNotNull(sourceHolderPriorityQueue.peek()); this.targetPreloadStatusOfCurrentPreloadingSource = targetPreloadStatusControl.getTargetPreloadStatus(preloadingHolder.rankingData); - preloadSourceInternal(preloadingHolder.mediaSource, preloadingHolder.startPositionUs); + if (targetPreloadStatusOfCurrentPreloadingSource != null) { + preloadSourceInternal(preloadingHolder.mediaSource, preloadingHolder.startPositionUs); + return true; + } } + return false; } /** A holder for information for preloading a single media source. */ 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 index 42a719ca2b..cb094a04fa 100644 --- 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 @@ -17,7 +17,6 @@ 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; @@ -233,12 +232,13 @@ public final class DefaultPreloadManager extends BasePreloadManager { @Nullable TargetPreloadStatusControl.PreloadStatus targetPreloadStatus = getTargetPreloadStatus(mediaSource); - checkState(targetPreloadStatus instanceof Status); - Status status = (Status) targetPreloadStatus; - if (continueLoadingPredicate.apply(checkNotNull(status))) { - return true; + if (targetPreloadStatus != null) { + Status status = (Status) targetPreloadStatus; + if (continueLoadingPredicate.apply(checkNotNull(status))) { + return true; + } + onPreloadCompleted(mediaSource); } - 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 index 01e441fcea..3aab985a17 100644 --- 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 @@ -15,16 +15,22 @@ */ package androidx.media3.exoplayer.source.preload; +import androidx.annotation.Nullable; import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.source.MediaSource; /** Controls the target preload status. */ @UnstableApi public interface TargetPreloadStatusControl { - /** Returns the target preload status for a source with the given {@code rankingData}. */ + /** + * Returns the target preload status for a source with the given {@code rankingData}. May be null + * if a {@link MediaSource} with the given {@code rankingData} should not be preloaded. + */ + @Nullable PreloadStatus getTargetPreloadStatus(T rankingData); - /** Defines the status of the preloading for a source. */ + /** Defines the status of the preloading for a {@link MediaSource}. */ interface PreloadStatus { /** The stage of the preloading. */ 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 index 1ff268ee31..788095a12b 100644 --- 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 @@ -166,10 +166,10 @@ public class DefaultPreloadManagerTest { @Test public void invalidate_withoutSettingCurrentPlayingIndex_sourcesPreloadedToTargetStatusesInOrder() { - ArrayList targetPreloadStatusControlCallReference = new ArrayList<>(); + ArrayList targetPreloadStatusControlCallStates = new ArrayList<>(); TargetPreloadStatusControl targetPreloadStatusControl = rankingData -> { - targetPreloadStatusControlCallReference.add(rankingData); + targetPreloadStatusControlCallStates.add(rankingData); return new DefaultPreloadManager.Status( DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED); }; @@ -190,7 +190,6 @@ public class DefaultPreloadManagerTest { 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); @@ -200,21 +199,22 @@ public class DefaultPreloadManagerTest { preloadManager.add(mediaItem2, /* rankingData= */ 2); FakeMediaSource wrappedMediaSource2 = fakeMediaSourceFactory.getLastCreatedSource(); wrappedMediaSource2.setAllowPreparation(false); + preloadManager.invalidate(); shadowOf(Looper.getMainLooper()).idle(); - assertThat(targetPreloadStatusControlCallReference).containsExactly(0); + assertThat(targetPreloadStatusControlCallStates).containsExactly(0); wrappedMediaSource0.setAllowPreparation(true); shadowOf(Looper.getMainLooper()).idle(); - assertThat(targetPreloadStatusControlCallReference).containsExactly(0, 1).inOrder(); + assertThat(targetPreloadStatusControlCallStates).containsExactly(0, 1).inOrder(); } @Test public void invalidate_withSettingCurrentPlayingIndex_sourcesPreloadedToTargetStatusesInOrder() { - ArrayList targetPreloadStatusControlCallReference = new ArrayList<>(); + ArrayList targetPreloadStatusControlCallStates = new ArrayList<>(); TargetPreloadStatusControl targetPreloadStatusControl = rankingData -> { - targetPreloadStatusControlCallReference.add(rankingData); + targetPreloadStatusControlCallStates.add(rankingData); return new DefaultPreloadManager.Status( DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED); }; @@ -235,7 +235,6 @@ public class DefaultPreloadManagerTest { 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); @@ -245,27 +244,27 @@ public class DefaultPreloadManagerTest { preloadManager.add(mediaItem2, /* rankingData= */ 2); FakeMediaSource wrappedMediaSource2 = fakeMediaSourceFactory.getLastCreatedSource(); wrappedMediaSource2.setAllowPreparation(false); - PreloadMediaSource preloadMediaSource2 = (PreloadMediaSource) preloadManager.getMediaSource(mediaItem2); preloadMediaSource2.prepareSource( (source, timeline) -> {}, bandwidthMeter.getTransferListener(), PlayerId.UNSET); preloadManager.setCurrentPlayingIndex(2); + preloadManager.invalidate(); shadowOf(Looper.getMainLooper()).idle(); - assertThat(targetPreloadStatusControlCallReference).containsExactly(2, 1); + assertThat(targetPreloadStatusControlCallStates).containsExactly(2, 1); wrappedMediaSource1.setAllowPreparation(true); shadowOf(Looper.getMainLooper()).idle(); - assertThat(targetPreloadStatusControlCallReference).containsExactly(2, 1, 0).inOrder(); + assertThat(targetPreloadStatusControlCallStates).containsExactly(2, 1, 0).inOrder(); } @Test public void invalidate_sourceHandedOverToPlayerDuringPreloading_continuesPreloadingNextSource() { - ArrayList targetPreloadStatusControlCallReference = new ArrayList<>(); + ArrayList targetPreloadStatusControlCallStates = new ArrayList<>(); TargetPreloadStatusControl targetPreloadStatusControl = rankingData -> { - targetPreloadStatusControlCallReference.add(rankingData); + targetPreloadStatusControlCallStates.add(rankingData); return new DefaultPreloadManager.Status( DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED); }; @@ -284,7 +283,6 @@ public class DefaultPreloadManagerTest { mediaItemBuilder.setMediaId("mediaId0").setUri("http://exoplayer.dev/video0").build(); MediaItem mediaItem1 = mediaItemBuilder.setMediaId("mediaId1").setUri("http://exoplayer.dev/video1").build(); - preloadManager.add(mediaItem0, /* rankingData= */ 0); FakeMediaSource wrappedMediaSource0 = fakeMediaSourceFactory.getLastCreatedSource(); wrappedMediaSource0.setAllowPreparation(false); @@ -292,7 +290,7 @@ public class DefaultPreloadManagerTest { FakeMediaSource wrappedMediaSource1 = fakeMediaSourceFactory.getLastCreatedSource(); wrappedMediaSource1.setAllowPreparation(false); preloadManager.invalidate(); - assertThat(targetPreloadStatusControlCallReference).containsExactly(0); + assertThat(targetPreloadStatusControlCallStates).containsExactly(0); PreloadMediaSource preloadMediaSource0 = (PreloadMediaSource) preloadManager.getMediaSource(mediaItem0); @@ -302,15 +300,15 @@ public class DefaultPreloadManagerTest { // The preload of mediaItem0 should complete and the preload manager continues to preload // mediaItem1, even when the preloadMediaSource0 hasn't finished preparation. - assertThat(targetPreloadStatusControlCallReference).containsExactly(0, 1).inOrder(); + assertThat(targetPreloadStatusControlCallStates).containsExactly(0, 1).inOrder(); } @Test public void invalidate_beforePreloadCompletedForLastInvalidate_preloadRespectsToLatestOrder() { - ArrayList targetPreloadStatusControlCallReference = new ArrayList<>(); + ArrayList targetPreloadStatusControlCallStates = new ArrayList<>(); TargetPreloadStatusControl targetPreloadStatusControl = rankingData -> { - targetPreloadStatusControlCallReference.add(rankingData); + targetPreloadStatusControlCallStates.add(rankingData); return new DefaultPreloadManager.Status( DefaultPreloadManager.Status.STAGE_TIMELINE_REFRESHED); }; @@ -340,18 +338,18 @@ 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(); shadowOf(Looper.getMainLooper()).idle(); - assertThat(targetPreloadStatusControlCallReference).containsExactly(0, 1).inOrder(); - targetPreloadStatusControlCallReference.clear(); + assertThat(targetPreloadStatusControlCallStates).containsExactly(0, 1).inOrder(); + targetPreloadStatusControlCallStates.clear(); preloadMediaSource0.releaseSource(externalCaller); PreloadMediaSource preloadMediaSource2 = (PreloadMediaSource) preloadManager.getMediaSource(mediaItem2); @@ -359,11 +357,57 @@ public class DefaultPreloadManagerTest { externalCaller, bandwidthMeter.getTransferListener(), PlayerId.UNSET); preloadManager.setCurrentPlayingIndex(2); preloadManager.invalidate(); + + // Simulate the delay of the preparation of wrappedMediaSource0, 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(targetPreloadStatusControlCallReference).containsExactly(2, 1).inOrder(); + assertThat(targetPreloadStatusControlCallStates).containsExactly(2, 1).inOrder(); wrappedMediaSource1.setAllowPreparation(true); shadowOf(Looper.getMainLooper()).idle(); - assertThat(targetPreloadStatusControlCallReference).containsExactly(2, 1, 0).inOrder(); + assertThat(targetPreloadStatusControlCallStates).containsExactly(2, 1, 0).inOrder(); + } + + @Test + public void invalidate_provideNullTargetPreloadStatus_sourcesSkippedForPreload() { + ArrayList targetPreloadStatusControlCallStates = new ArrayList<>(); + TargetPreloadStatusControl targetPreloadStatusControl = + rankingData -> { + targetPreloadStatusControlCallStates.add(rankingData); + return null; + }; + 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(targetPreloadStatusControlCallStates).containsExactly(0, 1, 2); } @Test @@ -404,11 +448,11 @@ public class DefaultPreloadManagerTest { } }; }); - preloadManager.add(mediaItem1, /* rankingData= */ 1); preloadManager.add(mediaItem2, /* rankingData= */ 2); preloadManager.invalidate(); shadowOf(Looper.getMainLooper()).idle(); + preloadManager.remove(mediaItem1); shadowOf(Looper.getMainLooper()).idle(); @@ -477,11 +521,11 @@ public class DefaultPreloadManagerTest { } }; }); - preloadManager.add(mediaItem1, /* rankingData= */ 1); preloadManager.add(mediaItem2, /* rankingData= */ 2); preloadManager.invalidate(); shadowOf(Looper.getMainLooper()).idle(); + preloadManager.release(); shadowOf(Looper.getMainLooper()).idle();