From 29d7dd1ec90496a02ae0e92b3de7debb1a63594e Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 15 Dec 2023 08:05:44 -0800 Subject: [PATCH] Keep selections for preloaded adaptive sample streams When an adaptive source has been preloaded, the track selection of the player should possibly not override the preloaded selection to avoid discarding preloaded data. PiperOrigin-RevId: 591256334 --- RELEASENOTES.md | 1 + .../source/preload/PreloadMediaPeriod.java | 122 ++- .../preload/PreloadMediaPeriodTest.java | 920 ++++++++++++++++-- .../media3/test/utils/FakeTrackSelection.java | 46 +- 4 files changed, 1004 insertions(+), 85 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 919890e525..73984c9649 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -40,6 +40,7 @@ negative values for `bufferedDurationUs` from chunk sources, resulting in an `IllegalArgumentException` ([#888](https://github.com/androidx/media/issues/888)). + * Support adaptive media sources with `PreloadMediaSource`. * Transformer: * Add support for flattening H.265/HEVC SEF slow motion videos. * Increase transmuxing speed, especially for 'remove video' edits. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriod.java index fde802795d..0a6b7d606f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriod.java @@ -16,8 +16,10 @@ package androidx.media3.exoplayer.source.preload; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; import androidx.annotation.Nullable; +import androidx.media3.common.C; import androidx.media3.common.util.NullableType; import androidx.media3.exoplayer.LoadingInfo; import androidx.media3.exoplayer.SeekParameters; @@ -27,7 +29,7 @@ import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.TrackSelectorResult; import java.io.IOException; -import java.util.Arrays; +import java.util.Objects; /** A {@link MediaPeriod} that has data preloaded before playback. */ /* package */ final class PreloadMediaPeriod implements MediaPeriod { @@ -105,32 +107,108 @@ import java.util.Arrays; @NullableType SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - long trackSelectionPositionUs; - if (preloadTrackSelectionHolder != null - && Arrays.equals(selections, preloadTrackSelectionHolder.trackSelectorResult.selections) - && positionUs == preloadTrackSelectionHolder.trackSelectionPositionUs) { - trackSelectionPositionUs = preloadTrackSelectionHolder.trackSelectionPositionUs; - System.arraycopy( - preloadTrackSelectionHolder.streams, - 0, - streams, - 0, - preloadTrackSelectionHolder.streams.length); - System.arraycopy( - preloadTrackSelectionHolder.streamResetFlags, - 0, - streamResetFlags, - 0, - preloadTrackSelectionHolder.streamResetFlags.length); - } else { + if (preloadTrackSelectionHolder == null) { + // No preload track selection was done. + return mediaPeriod.selectTracks( + selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs); + } + checkState(streams.length == preloadTrackSelectionHolder.streams.length); + if (positionUs != preloadTrackSelectionHolder.trackSelectionPositionUs) { + // Position changed. Copy formerly preloaded sample streams to the track selection properties + // to make sure we give the period the chance to release discarded sample streams. + for (int i = 0; i < preloadTrackSelectionHolder.streams.length; i++) { + if (preloadTrackSelectionHolder.streams[i] != null) { + streams[i] = preloadTrackSelectionHolder.streams[i]; + mayRetainStreamFlags[i] = false; + } + } + preloadTrackSelectionHolder = null; + return mediaPeriod.selectTracks( + selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs); + } + PreloadTrackSelectionHolder holder = checkNotNull(preloadTrackSelectionHolder); + long trackSelectionPositionUs = holder.trackSelectionPositionUs; + boolean[] preloadStreamResetFlags = holder.streamResetFlags; + if (maybeUpdatePreloadTrackSelectionHolderForReselection(selections, holder)) { + // Preload can only be partially reused. Select tracks again attempting to retain preloads. + preloadStreamResetFlags = new boolean[preloadStreamResetFlags.length]; trackSelectionPositionUs = mediaPeriod.selectTracks( - selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs); + holder.trackSelectorResult.selections, + holder.mayRetainStreamFlags, + holder.streams, + preloadStreamResetFlags, + holder.trackSelectionPositionUs); + // Make sure to set the reset flags for streams we preloaded. + for (int i = 0; i < holder.mayRetainStreamFlags.length; i++) { + if (holder.mayRetainStreamFlags[i]) { + preloadStreamResetFlags[i] = true; + } + } } + System.arraycopy(holder.streams, 0, streams, 0, holder.streams.length); + System.arraycopy( + preloadStreamResetFlags, 0, streamResetFlags, 0, preloadStreamResetFlags.length); preloadTrackSelectionHolder = null; return trackSelectionPositionUs; } + private static boolean maybeUpdatePreloadTrackSelectionHolderForReselection( + @NullableType ExoTrackSelection[] selections, + PreloadTrackSelectionHolder preloadTrackSelectionHolder) { + @NullableType + ExoTrackSelection[] preloadSelections = + checkNotNull(preloadTrackSelectionHolder).trackSelectorResult.selections; + boolean needsReselection = false; + for (int i = 0; i < selections.length; i++) { + ExoTrackSelection selection = selections[i]; + ExoTrackSelection preloadSelection = preloadSelections[i]; + if (selection == null && preloadSelection == null) { + continue; + } + preloadTrackSelectionHolder.mayRetainStreamFlags[i] = false; + if (selection == null) { + // Preloaded track got disabled. Discard preloaded stream. + preloadTrackSelectionHolder.trackSelectorResult.selections[i] = null; + needsReselection = true; + } else if (preloadSelection == null) { + // Enabled track not preloaded. Update selection. + preloadTrackSelectionHolder.trackSelectorResult.selections[i] = selection; + needsReselection = true; + } else if (!isSameAdaptionSet(selection, preloadSelection)) { + // Adaption set has changed. Discard preloaded stream. + preloadTrackSelectionHolder.trackSelectorResult.selections[i] = selection; + needsReselection = true; + } else if (selection.getTrackGroup().type == C.TRACK_TYPE_VIDEO + || selection.getTrackGroup().type == C.TRACK_TYPE_AUDIO + || selection.getSelectedIndexInTrackGroup() + == preloadSelection.getSelectedIndexInTrackGroup()) { + // The selection in a audio or video track has changed or it hasn't changed. Set the retain + // flag in case we reselect with a partially preloaded streams set. + preloadTrackSelectionHolder.mayRetainStreamFlags[i] = true; + } else { + // The selection in a non-audio or non-video track has changed. Discard preloaded stream. + preloadTrackSelectionHolder.trackSelectorResult.selections[i] = selection; + needsReselection = true; + } + } + return needsReselection; + } + + private static boolean isSameAdaptionSet( + ExoTrackSelection selection, ExoTrackSelection preloadSelection) { + if (!Objects.equals(selection.getTrackGroup(), preloadSelection.getTrackGroup()) + || selection.length() != preloadSelection.length()) { + return false; + } + for (int i = 0; i < selection.length(); i++) { + if (selection.getIndexInTrackGroup(i) != preloadSelection.getIndexInTrackGroup(i)) { + return false; + } + } + return true; + } + /* package */ long selectTracksForPreloading( TrackSelectorResult trackSelectorResult, long positionUs) { @NullableType ExoTrackSelection[] selections = trackSelectorResult.selections; @@ -154,6 +232,7 @@ import java.util.Arrays; preloadTrackSelectionHolder = new PreloadTrackSelectionHolder( trackSelectorResult, + mayRetainStreamFlags, preloadedSampleStreams, preloadedStreamResetFlags, trackSelectionPositionUs); @@ -207,16 +286,19 @@ import java.util.Arrays; private static class PreloadTrackSelectionHolder { public final TrackSelectorResult trackSelectorResult; + public final boolean[] mayRetainStreamFlags; public final @NullableType SampleStream[] streams; public final boolean[] streamResetFlags; public final long trackSelectionPositionUs; public PreloadTrackSelectionHolder( TrackSelectorResult trackSelectorResult, + boolean[] mayRetainStreamFlags, @NullableType SampleStream[] streams, boolean[] streamResetFlags, long trackSelectionPositionUs) { this.trackSelectorResult = trackSelectorResult; + this.mayRetainStreamFlags = mayRetainStreamFlags; this.streams = streams; this.streamResetFlags = streamResetFlags; this.trackSelectionPositionUs = trackSelectionPositionUs; diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriodTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriodTest.java index 3d07246660..d64238d540 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriodTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaPeriodTest.java @@ -21,8 +21,10 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; @@ -47,11 +49,13 @@ import androidx.media3.exoplayer.trackselection.TrackSelectorResult; import androidx.media3.exoplayer.upstream.DefaultAllocator; import androidx.media3.test.utils.FakeMediaPeriod; import androidx.media3.test.utils.FakeTimeline; +import androidx.media3.test.utils.FakeTrackSelection; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; /** Unit test for {@link PreloadMediaPeriod}. */ @RunWith(AndroidJUnit4.class) @@ -220,9 +224,9 @@ public final class PreloadMediaPeriodTest { } @Test - public void selectTracks_afterPreloadingForSameSelections_usePreloadedResults() { + public void selectTracks_afterPreloadingForSameSelections_usePreloadedStreams() { MediaPeriod wrappedMediaPeriod = mock(MediaPeriod.class); - ExoTrackSelection[] trackSelections = + ExoTrackSelection[] preloadTrackSelections = new ExoTrackSelection[] { new FixedTrackSelection( new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build()), @@ -231,25 +235,28 @@ public final class PreloadMediaPeriodTest { new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build()), /* track= */ 0) }; - TrackSelectorResult trackSelectorResult = + TrackSelectorResult preloadTrackSelectorResult = new TrackSelectorResult( new RendererConfiguration[] { RendererConfiguration.DEFAULT, RendererConfiguration.DEFAULT }, - trackSelections, + preloadTrackSelections, Tracks.EMPTY, /* info= */ null); SampleStream[] preloadedStreams = new SampleStream[] {new EmptySampleStream(), new EmptySampleStream()}; when(wrappedMediaPeriod.selectTracks( - eq(trackSelections), any(), any(), any(), /* positionUs= */ eq(0L))) + eq(preloadTrackSelections), any(), any(), any(), /* positionUs= */ eq(0L))) .thenAnswer( invocation -> { + boolean[] mayRetainStreamFlags = invocation.getArgument(1); SampleStream[] streams = invocation.getArgument(2); boolean[] streamResetFlags = invocation.getArgument(3); for (int i = 0; i < streams.length; i++) { - streams[i] = preloadedStreams[i]; - streamResetFlags[i] = true; + if (!mayRetainStreamFlags[i]) { + streams[i] = preloadedStreams[i]; + streamResetFlags[i] = true; + } } return 0L; }); @@ -263,48 +270,465 @@ public final class PreloadMediaPeriodTest { public void onContinueLoadingRequested(MediaPeriod source) {} }; preloadMediaPeriod.prepare(callback, /* positionUs= */ 0L); - // Select tracks for preloading. long preloadTrackSelectionStartPositionUs = - preloadMediaPeriod.selectTracksForPreloading(trackSelectorResult, /* positionUs= */ 0L); + preloadMediaPeriod.selectTracksForPreloading( + preloadTrackSelectorResult, /* positionUs= */ 0L); SampleStream[] streams = new SampleStream[2]; boolean[] streamResetFlags = new boolean[2]; + // Select tracks based on the same track selections. long trackSelectionStartPositionUs = preloadMediaPeriod.selectTracks( - trackSelections, new boolean[2], streams, streamResetFlags, /* positionUs= */ 0L); + preloadTrackSelections, + new boolean[2], + streams, + streamResetFlags, + /* positionUs= */ 0L); + verify(wrappedMediaPeriod).prepare(any(), anyLong()); verify(wrappedMediaPeriod) - .selectTracks(eq(trackSelections), any(), any(), any(), /* positionUs= */ eq(0L)); + .selectTracks(eq(preloadTrackSelections), any(), any(), any(), /* positionUs= */ eq(0L)); + verifyNoMoreInteractions(wrappedMediaPeriod); assertThat(trackSelectionStartPositionUs).isEqualTo(preloadTrackSelectionStartPositionUs); assertThat(streams).isEqualTo(preloadedStreams); - assertThat(streamResetFlags).hasLength(2); - assertThat(streamResetFlags[0]).isTrue(); - assertThat(streamResetFlags[1]).isTrue(); + assertThat(streamResetFlags).asList().containsExactly(true, true); } @Test - public void selectTracks_afterPreloadingButForDifferentSelections_callOnWrappedPeriod() { + public void + selectTracks_afterPreloadingForDifferentSelections_callOnWrappedPeriodRetainingPreloadedStreams() { + MediaPeriod wrappedMediaPeriod = mock(MediaPeriod.class); + ExoTrackSelection[] preloadTrackSelections = + new ExoTrackSelection[] { + new FakeTrackSelection( + new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( + new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage("de") + .build()), + /* selectedIndex= */ 0) + }; + TrackSelectorResult preloadTrackSelectorResult = + new TrackSelectorResult( + new RendererConfiguration[] { + RendererConfiguration.DEFAULT, + RendererConfiguration.DEFAULT, + RendererConfiguration.DEFAULT + }, + preloadTrackSelections, + Tracks.EMPTY, + /* info= */ null); + SampleStream[] preloadedStreams = + new SampleStream[] { + new EmptySampleStream(), new EmptySampleStream(), new EmptySampleStream() + }; + when(wrappedMediaPeriod.selectTracks( + eq(preloadTrackSelections), + /* mayRetainStreamFlags= */ eq(new boolean[] {false, false, false}), + /* streams= */ any(), + /* streamResetFlags= */ any(), + /* positionUs= */ eq(0L))) + .thenAnswer( + invocation -> { + boolean[] mayRetainStreamFlags = invocation.getArgument(1); + SampleStream[] streams = invocation.getArgument(2); + boolean[] streamResetFlags = invocation.getArgument(3); + for (int i = 0; i < streams.length; i++) { + if (!mayRetainStreamFlags[i]) { + streams[i] = preloadedStreams[i]; + streamResetFlags[i] = true; + } + } + return 0L; + }); + PreloadMediaPeriod preloadMediaPeriod = new PreloadMediaPeriod(wrappedMediaPeriod); + MediaPeriod.Callback callback = + new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) {} + + @Override + public void onContinueLoadingRequested(MediaPeriod source) {} + }; + preloadMediaPeriod.prepare(callback, /* positionUs= */ 0L); + // Select tracks for preloading. + preloadMediaPeriod.selectTracksForPreloading(preloadTrackSelectorResult, /* positionUs= */ 0L); + verify(wrappedMediaPeriod).prepare(any(), anyLong()); + verify(wrappedMediaPeriod) + .selectTracks( + eq(preloadTrackSelections), + /* mayRetainStreamFlags= */ eq(new boolean[] {false, false, false}), + eq(preloadedStreams), + any(), + eq(0L)); + verifyNoMoreInteractions(wrappedMediaPeriod); + reset(wrappedMediaPeriod); + // Create a new track selections. + ExoTrackSelection[] newTrackSelections = + new ExoTrackSelection[] { + new FakeTrackSelection( + new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( + new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage("es") + .build()), + /* selectedIndex= */ 0) + }; + SampleStream[] newSampleStreams = + new SampleStream[] { + new EmptySampleStream(), new EmptySampleStream(), new EmptySampleStream() + }; + when(wrappedMediaPeriod.selectTracks( + eq(newTrackSelections), + /* mayRetainStreamFlags= */ eq(new boolean[] {true, true, false}), + /* streams= */ any(), + /* streamResetFlags= */ any(), + /* positionUs= */ eq(0L))) + .thenAnswer( + invocation -> { + boolean[] mayRetainStreamFlags = invocation.getArgument(1); + SampleStream[] streams = invocation.getArgument(2); + boolean[] streamResetFlags = invocation.getArgument(3); + for (int i = 0; i < streams.length; i++) { + if (!mayRetainStreamFlags[i]) { + streams[i] = newSampleStreams[i]; + streamResetFlags[i] = true; + } + } + return 0L; + }); + SampleStream[] streams = new SampleStream[3]; + boolean[] streamResetFlags = new boolean[3]; + + // Select tracks based on the new track selections. + long trackSelectionStartPositionUs = + preloadMediaPeriod.selectTracks( + newTrackSelections, new boolean[3], streams, streamResetFlags, /* positionUs= */ 0L); + + verify(wrappedMediaPeriod) + .selectTracks( + eq( + new ExoTrackSelection[] { + preloadTrackSelections[0], preloadTrackSelections[1], newTrackSelections[2] + }), + /* mayRetainStreamFlags= */ eq(new boolean[] {true, true, false}), + eq(new SampleStream[] {preloadedStreams[0], preloadedStreams[1], newSampleStreams[2]}), + /* streamResetFlags= */ any(), + /* positionUs= */ eq(0L)); + verifyNoMoreInteractions(wrappedMediaPeriod); + assertThat(trackSelectionStartPositionUs).isEqualTo(0L); + // Use newSampleStreams instead of preloadedSampleStreams for the text track. + assertThat(streams) + .isEqualTo( + new SampleStream[] {preloadedStreams[0], preloadedStreams[1], newSampleStreams[2]}); + assertThat(streamResetFlags).asList().containsExactly(true, true, true); + } + + @Test + public void selectTracks_afterPreloadingForDifferentAdaptiveVideoSelection_usePreloadedStreams() { + MediaPeriod wrappedMediaPeriod = mock(MediaPeriod.class); + ExoTrackSelection[] preloadTrackSelections = + new ExoTrackSelection[] { + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setWidth(1920) + .setHeight(1080) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build(), + new Format.Builder() + .setWidth(3840) + .setHeight(2160) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build()), + /* selectedIndex= */ 1), + new FakeTrackSelection( + new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage("de") + .build()), + /* selectedIndex= */ 0) + }; + TrackSelectorResult preloadTrackSelectorResult = + new TrackSelectorResult( + new RendererConfiguration[] { + RendererConfiguration.DEFAULT, + RendererConfiguration.DEFAULT, + RendererConfiguration.DEFAULT + }, + preloadTrackSelections, + Tracks.EMPTY, + /* info= */ null); + SampleStream[] preloadedStreams = + new SampleStream[] { + new EmptySampleStream(), new EmptySampleStream(), new EmptySampleStream() + }; + when(wrappedMediaPeriod.selectTracks( + eq(preloadTrackSelections), any(), any(), any(), /* positionUs= */ eq(0L))) + .thenAnswer( + invocation -> { + boolean[] mayRetainStreamFlags = invocation.getArgument(1); + SampleStream[] streams = invocation.getArgument(2); + boolean[] streamResetFlags = invocation.getArgument(3); + for (int i = 0; i < streams.length; i++) { + if (!mayRetainStreamFlags[i]) { + streams[i] = preloadedStreams[i]; + streamResetFlags[i] = true; + } + } + return 0L; + }); + PreloadMediaPeriod preloadMediaPeriod = new PreloadMediaPeriod(wrappedMediaPeriod); + MediaPeriod.Callback callback = + new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) {} + + @Override + public void onContinueLoadingRequested(MediaPeriod source) {} + }; + preloadMediaPeriod.prepare(callback, /* positionUs= */ 0L); + // Select tracks for preloading. + preloadMediaPeriod.selectTracksForPreloading(preloadTrackSelectorResult, /* positionUs= */ 0L); + verify(wrappedMediaPeriod).prepare(any(), anyLong()); + verify(wrappedMediaPeriod) + .selectTracks( + eq(preloadTrackSelections), + /* mayRetainStreamFlags= */ eq(new boolean[] {false, false, false}), + /* streams= */ any(), + /* streamResetFlags= */ any(), + /* positionUs= */ eq(0L)); + // Create a new track selections. + ExoTrackSelection[] newTrackSelections = + new ExoTrackSelection[] { + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setWidth(1920) + .setHeight(1080) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build(), + new Format.Builder() + .setWidth(3840) + .setHeight(2160) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( + new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage("de") + .build()), + /* selectedIndex= */ 0) + }; + SampleStream[] streams = new SampleStream[3]; + boolean[] streamResetFlags = new boolean[3]; + + // Track selection from player must not call the wrapped media period again. + long trackSelectionStartPositionUs = + preloadMediaPeriod.selectTracks( + newTrackSelections, new boolean[3], streams, streamResetFlags, /* positionUs= */ 0L); + + verifyNoMoreInteractions(wrappedMediaPeriod); + assertThat(trackSelectionStartPositionUs).isEqualTo(0L); + assertThat(streams).isEqualTo(preloadedStreams); + assertThat(streamResetFlags).asList().containsExactly(true, true, true); + } + + @Test + public void selectTracks_afterPreloadingForDifferentAdaptiveAudioSelection_usePreloadStreams() { + MediaPeriod wrappedMediaPeriod = mock(MediaPeriod.class); + ExoTrackSelection[] preloadTrackSelections = + new ExoTrackSelection[] { + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setWidth(1920) + .setHeight(1080) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build(), + new Format.Builder() + .setWidth(3840) + .setHeight(2160) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( + new TrackGroup( + new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build(), + new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_E_AC3_JOC).build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage("de") + .build()), + /* selectedIndex= */ 0) + }; + TrackSelectorResult preloadTrackSelectorResult = + new TrackSelectorResult( + new RendererConfiguration[] { + RendererConfiguration.DEFAULT, + RendererConfiguration.DEFAULT, + RendererConfiguration.DEFAULT + }, + preloadTrackSelections, + Tracks.EMPTY, + /* info= */ null); + SampleStream[] preloadedStreams = + new SampleStream[] { + new EmptySampleStream(), new EmptySampleStream(), new EmptySampleStream() + }; + when(wrappedMediaPeriod.selectTracks( + eq(preloadTrackSelections), any(), any(), any(), /* positionUs= */ eq(0L))) + .thenAnswer( + invocation -> { + boolean[] mayRetainStreamFlags = invocation.getArgument(1); + SampleStream[] streams = invocation.getArgument(2); + boolean[] streamResetFlags = invocation.getArgument(3); + for (int i = 0; i < streams.length; i++) { + if (!mayRetainStreamFlags[i]) { + streams[i] = preloadedStreams[i]; + streamResetFlags[i] = true; + } + } + return 0L; + }); + PreloadMediaPeriod preloadMediaPeriod = new PreloadMediaPeriod(wrappedMediaPeriod); + MediaPeriod.Callback callback = + new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) {} + + @Override + public void onContinueLoadingRequested(MediaPeriod source) {} + }; + preloadMediaPeriod.prepare(callback, /* positionUs= */ 0L); + // Select tracks for preloading. + preloadMediaPeriod.selectTracksForPreloading(preloadTrackSelectorResult, /* positionUs= */ 0L); + verify(wrappedMediaPeriod).prepare(any(), anyLong()); + verify(wrappedMediaPeriod) + .selectTracks( + eq(preloadTrackSelections), + /* mayRetainStreamFlags= */ eq(new boolean[] {false, false, false}), + /* streams= */ any(), + /* streamResetFlags= */ any(), + /* positionUs= */ eq(0L)); + // Create a new track selections. + ExoTrackSelection[] newTrackSelections = + new ExoTrackSelection[] { + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setWidth(1920) + .setHeight(1080) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build(), + new Format.Builder() + .setWidth(3840) + .setHeight(2160) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( + new TrackGroup( + new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build(), + new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_E_AC3_JOC).build()), + /* selectedIndex= */ 1), + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage("de") + .build()), + /* selectedIndex= */ 0) + }; + SampleStream[] streams = new SampleStream[3]; + boolean[] streamResetFlags = new boolean[3]; + + // Track selection from player must not call the wrapped media period again. + long trackSelectionStartPositionUs = + preloadMediaPeriod.selectTracks( + newTrackSelections, new boolean[3], streams, streamResetFlags, /* positionUs= */ 0L); + + verifyNoMoreInteractions(wrappedMediaPeriod); + assertThat(trackSelectionStartPositionUs).isEqualTo(0L); + assertThat(streams).isEqualTo(preloadedStreams); + assertThat(streamResetFlags).asList().containsExactly(true, true, true); + } + + @Test + public void + selectTracks_afterPreloadingForDifferentAdaptiveTextSelection_callOnWrappedPeriodRetainingPreloadedStreams() { MediaPeriod wrappedMediaPeriod = mock(MediaPeriod.class); ExoTrackSelection[] trackSelections = new ExoTrackSelection[] { - new FixedTrackSelection( + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setWidth(1920) + .setHeight(1080) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build(), + new Format.Builder() + .setWidth(3840) + .setHeight(2160) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build()), - /* track= */ 0), - new FixedTrackSelection( - new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build()), - /* track= */ 0) + /* selectedIndex= */ 0), // + // An adaptive track group for text is practically not possible. The hypothetical case has + // been created for test coverage. + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage("de") + .build(), + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage("en") + .build()), + /* selectedIndex= */ 0) }; TrackSelectorResult trackSelectorResult = new TrackSelectorResult( new RendererConfiguration[] { - RendererConfiguration.DEFAULT, RendererConfiguration.DEFAULT + RendererConfiguration.DEFAULT, + RendererConfiguration.DEFAULT, + RendererConfiguration.DEFAULT }, trackSelections, Tracks.EMPTY, /* info= */ null); SampleStream[] preloadedStreams = - new SampleStream[] {new EmptySampleStream(), new EmptySampleStream()}; + new SampleStream[] { + new EmptySampleStream(), new EmptySampleStream(), new EmptySampleStream() + }; when(wrappedMediaPeriod.selectTracks( eq(trackSelections), any(), any(), any(), /* positionUs= */ eq(0L))) .thenAnswer( @@ -327,71 +751,415 @@ public final class PreloadMediaPeriodTest { public void onContinueLoadingRequested(MediaPeriod source) {} }; preloadMediaPeriod.prepare(callback, /* positionUs= */ 0L); - // Select tracks for preloading. preloadMediaPeriod.selectTracksForPreloading(trackSelectorResult, /* positionUs= */ 0L); + verify(wrappedMediaPeriod).prepare(any(), anyLong()); verify(wrappedMediaPeriod) .selectTracks(eq(trackSelections), any(), any(), any(), /* positionUs= */ eq(0L)); + verifyNoMoreInteractions(wrappedMediaPeriod); + reset(wrappedMediaPeriod); // Create a new track selections. ExoTrackSelection[] newTrackSelections = new ExoTrackSelection[] { - null, - new FixedTrackSelection( - new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build()), - /* track= */ 0) + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setWidth(1920) + .setHeight(1080) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build(), + new Format.Builder() + .setWidth(3840) + .setHeight(2160) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( + new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build()), + /* selectedIndex= */ 0), + // An adaptive track group for text is practically not possible. The hypothetical case has + // been created for test coverage. + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage("de") + .build(), + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage("en") + .build()), + /* selectedIndex= */ 1) + }; + SampleStream[] newSampleStreams = + new SampleStream[] { + new EmptySampleStream(), new EmptySampleStream(), new EmptySampleStream() }; - SampleStream[] newSampleStreams = new SampleStream[] {new EmptySampleStream()}; when(wrappedMediaPeriod.selectTracks( eq(newTrackSelections), any(), any(), any(), /* positionUs= */ eq(0L))) .thenAnswer( invocation -> { + boolean[] mayRetainStreamFlags = invocation.getArgument(1); SampleStream[] streams = invocation.getArgument(2); boolean[] streamResetFlags = invocation.getArgument(3); for (int i = 0; i < streams.length; i++) { - streams[i] = newSampleStreams[i]; + if (!mayRetainStreamFlags[i]) { + streams[i] = newSampleStreams[i]; + streamResetFlags[i] = true; + } + } + return 0L; + }); + SampleStream[] streams = new SampleStream[3]; + boolean[] streamResetFlags = new boolean[3]; + + // Track selection from player must call the wrapped media period again. + long trackSelectionStartPositionUs = + preloadMediaPeriod.selectTracks( + newTrackSelections, new boolean[3], streams, streamResetFlags, /* positionUs= */ 0L); + + verify(wrappedMediaPeriod) + .selectTracks( + eq( + new ExoTrackSelection[] { + trackSelections[0], trackSelections[1], newTrackSelections[2] + }), + /* mayRetainStreamFlags= */ eq(new boolean[] {true, true, false}), + /* streams= */ eq( + new SampleStream[] {preloadedStreams[0], preloadedStreams[1], newSampleStreams[2]}), + /* streamResetFlags= */ any(), + /* positionUs= */ eq(0L)); + verifyNoMoreInteractions(wrappedMediaPeriod); + assertThat(trackSelectionStartPositionUs).isEqualTo(0L); + assertThat(streams) + .isEqualTo( + new SampleStream[] {preloadedStreams[0], preloadedStreams[1], newSampleStreams[2]}); + assertThat(streamResetFlags).asList().containsExactly(true, true, true); + } + + @Test + public void + selectTracks_afterPreloadingWithAudioDisabled_callOnWrappedPeriodRetainingPreloadedStreams() { + MediaPeriod wrappedMediaPeriod = mock(MediaPeriod.class); + ExoTrackSelection[] preloadTrackSelections = + new ExoTrackSelection[] { + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setWidth(1920) + .setHeight(1080) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( + new TrackGroup( + new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_E_AC3_JOC).build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage("de") + .build()), + /* selectedIndex= */ 0) + }; + TrackSelectorResult preloadTrackSelectorResult = + new TrackSelectorResult( + new RendererConfiguration[] { + RendererConfiguration.DEFAULT, + RendererConfiguration.DEFAULT, + RendererConfiguration.DEFAULT + }, + preloadTrackSelections, + Tracks.EMPTY, + /* info= */ null); + SampleStream[] preloadedStreams = + new SampleStream[] { + new EmptySampleStream(), new EmptySampleStream(), new EmptySampleStream() + }; + when(wrappedMediaPeriod.selectTracks( + eq(preloadTrackSelections), any(), any(), any(), /* positionUs= */ eq(0L))) + .thenAnswer( + invocation -> { + SampleStream[] streams = invocation.getArgument(2); + boolean[] streamResetFlags = invocation.getArgument(3); + for (int i = 0; i < streams.length; i++) { + streams[i] = preloadedStreams[i]; streamResetFlags[i] = true; } return 0L; }); - SampleStream[] streams = new SampleStream[1]; - boolean[] streamResetFlags = new boolean[1]; - // Select tracks based on the new track selections. + PreloadMediaPeriod preloadMediaPeriod = new PreloadMediaPeriod(wrappedMediaPeriod); + MediaPeriod.Callback callback = + new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) {} + + @Override + public void onContinueLoadingRequested(MediaPeriod source) {} + }; + preloadMediaPeriod.prepare(callback, /* positionUs= */ 0L); + // Select tracks for preloading. + preloadMediaPeriod.selectTracksForPreloading(preloadTrackSelectorResult, /* positionUs= */ 0L); + verify(wrappedMediaPeriod).prepare(any(), anyLong()); + verify(wrappedMediaPeriod) + .selectTracks( + eq(preloadTrackSelections), + eq(new boolean[] {false, false, false}), + any(), + any(), + /* positionUs= */ eq(0L)); + verifyNoMoreInteractions(wrappedMediaPeriod); + reset(wrappedMediaPeriod); + // Create a new track selection. + ExoTrackSelection[] trackSelectionWithAudioDisabled = + new ExoTrackSelection[] { + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setWidth(1920) + .setHeight(1080) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build()), + /* selectedIndex= */ 0), + null, + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage("de") + .build()), + /* selectedIndex= */ 0) + }; + SampleStream[] newStreams = + new SampleStream[] {new EmptySampleStream(), null, new EmptySampleStream()}; + ExoTrackSelection[] expectedTrackSelection = + new ExoTrackSelection[] {preloadTrackSelections[0], null, preloadTrackSelections[2]}; + when(wrappedMediaPeriod.selectTracks( + eq(expectedTrackSelection), any(), any(), any(), /* positionUs= */ eq(0L))) + .thenAnswer( + invocation -> { + boolean[] mayRetainStreamFlags = invocation.getArgument(1); + SampleStream[] streams = invocation.getArgument(2); + boolean[] streamResetFlags = invocation.getArgument(3); + for (int i = 0; i < streams.length; i++) { + if (!mayRetainStreamFlags[i]) { + streams[i] = newStreams[i]; + streamResetFlags[i] = newStreams[i] != null; + } + } + return 0L; + }); + SampleStream[] streams = new SampleStream[3]; + boolean[] streamResetFlags = new boolean[3]; + + // Track selection from player must call the wrapped media period again. long trackSelectionStartPositionUs = preloadMediaPeriod.selectTracks( - newTrackSelections, new boolean[1], streams, streamResetFlags, /* positionUs= */ 0L); + trackSelectionWithAudioDisabled, + new boolean[3], + streams, + streamResetFlags, + /* positionUs= */ 0L); verify(wrappedMediaPeriod) - .selectTracks(eq(newTrackSelections), any(), same(streams), same(streamResetFlags), eq(0L)); + .selectTracks( + eq(expectedTrackSelection), + /* mayRetainStreamFlags= */ eq(new boolean[] {true, false, true}), + /* streams= */ any(), + /* streamResetFlags= */ any(), + /* positionUs= */ eq(0L)); + verifyNoMoreInteractions(wrappedMediaPeriod); assertThat(trackSelectionStartPositionUs).isEqualTo(0L); - // Use newSampleStreams instead of preloadedSampleStreams - assertThat(streams).isEqualTo(newSampleStreams); - assertThat(streamResetFlags).hasLength(1); - assertThat(streamResetFlags[0]).isTrue(); + assertThat(streams) + .isEqualTo(new SampleStream[] {preloadedStreams[0], null, preloadedStreams[2]}); + assertThat(streamResetFlags).asList().containsExactly(true, false, true); + } + + @Test + public void + selectTracks_afterPreloadingWithAudioEnabled_callOnWrappedPeriodRetainingPreloadedStreams() { + MediaPeriod wrappedMediaPeriod = mock(MediaPeriod.class); + ExoTrackSelection[] preloadTrackSelections = + new ExoTrackSelection[] { + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setWidth(1920) + .setHeight(1080) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build()), + /* selectedIndex= */ 0), + null, + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage("de") + .build()), + /* selectedIndex= */ 0) + }; + TrackSelectorResult preloadTrackSelectorResult = + new TrackSelectorResult( + new RendererConfiguration[] { + RendererConfiguration.DEFAULT, + RendererConfiguration.DEFAULT, + RendererConfiguration.DEFAULT + }, + preloadTrackSelections, + Tracks.EMPTY, + /* info= */ null); + SampleStream[] preloadedStreams = + new SampleStream[] {new EmptySampleStream(), null, new EmptySampleStream()}; + when(wrappedMediaPeriod.selectTracks( + eq(preloadTrackSelections), + /* mayRetainStreamFlags= */ eq(new boolean[] {false, false, false}), + /* streams= */ eq(new SampleStream[3]), + /* streamResetFlags= */ any(), + /* positionUs= */ eq(0L))) + .thenAnswer( + invocation -> { + SampleStream[] streams = invocation.getArgument(2); + boolean[] streamResetFlags = invocation.getArgument(3); + for (int i = 0; i < streams.length; i++) { + streams[i] = preloadedStreams[i]; + streamResetFlags[i] = preloadedStreams != null; + } + return 0L; + }); + PreloadMediaPeriod preloadMediaPeriod = new PreloadMediaPeriod(wrappedMediaPeriod); + MediaPeriod.Callback callback = + new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) {} + + @Override + public void onContinueLoadingRequested(MediaPeriod source) {} + }; + preloadMediaPeriod.prepare(callback, /* positionUs= */ 0L); + // Select tracks for preloading. + preloadMediaPeriod.selectTracksForPreloading(preloadTrackSelectorResult, /* positionUs= */ 0L); + verify(wrappedMediaPeriod).prepare(any(), anyLong()); + verify(wrappedMediaPeriod) + .selectTracks( + eq(preloadTrackSelections), + /* mayRetainStreamFlags= */ eq(new boolean[] {false, false, false}), + /* streams= */ any(), + /* streamResetFlags= */ any(), + /* positionUs= */ eq(0L)); + verifyNoMoreInteractions(wrappedMediaPeriod); + Mockito.reset(wrappedMediaPeriod); + // Create a new track selection. + ExoTrackSelection[] trackSelectionWithAudioEnabled = + new ExoTrackSelection[] { + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setWidth(1920) + .setHeight(1080) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( + new TrackGroup( + new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_E_AC3_JOC).build()), + /* selectedIndex= */ 0), + new FakeTrackSelection( + new TrackGroup( + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage("de") + .build()), + /* selectedIndex= */ 0) + }; + SampleStream[] newStreams = + new SampleStream[] { + new EmptySampleStream(), new EmptySampleStream(), new EmptySampleStream() + }; + ExoTrackSelection[] expectedTrackSelection = + new ExoTrackSelection[] { + preloadTrackSelections[0], trackSelectionWithAudioEnabled[1], preloadTrackSelections[2] + }; + when(wrappedMediaPeriod.selectTracks( + eq(expectedTrackSelection), any(), any(), any(), /* positionUs= */ eq(0L))) + .thenAnswer( + invocation -> { + boolean[] mayRetainStreamFlags = invocation.getArgument(1); + SampleStream[] streams = invocation.getArgument(2); + boolean[] streamResetFlags = invocation.getArgument(3); + for (int i = 0; i < streams.length; i++) { + if (!mayRetainStreamFlags[i]) { + streams[i] = newStreams[i]; + streamResetFlags[i] = true; + } + } + return 0L; + }); + SampleStream[] streams = new SampleStream[3]; + boolean[] streamResetFlags = new boolean[3]; + + // Track selection from player must call the wrapped media period again. + long trackSelectionStartPositionUs = + preloadMediaPeriod.selectTracks( + trackSelectionWithAudioEnabled, + /* mayRetainStreamFlags= */ new boolean[3], + streams, + streamResetFlags, + /* positionUs= */ 0L); + + verify(wrappedMediaPeriod) + .selectTracks( + eq(expectedTrackSelection), + /* mayRetainStreamFlags= */ eq(new boolean[] {true, false, true}), + /* streams= */ eq( + new SampleStream[] {preloadedStreams[0], newStreams[1], preloadedStreams[2]}), + /* streamResetFlags= */ any(), + /* positionUs= */ eq(0L)); + verifyNoMoreInteractions(wrappedMediaPeriod); + assertThat(trackSelectionStartPositionUs).isEqualTo(0L); + assertThat(streams) + .isEqualTo(new SampleStream[] {preloadedStreams[0], newStreams[1], preloadedStreams[2]}); + assertThat(streamResetFlags).asList().containsExactly(true, true, true); } @Test public void selectTracks_afterPreloadingForSameSelectionsButAtDifferentPosition_callOnWrappedPeriod() { MediaPeriod wrappedMediaPeriod = mock(MediaPeriod.class); - ExoTrackSelection[] trackSelections = + ExoTrackSelection[] preloadTrackSelections = new ExoTrackSelection[] { - new FixedTrackSelection( + new FakeTrackSelection( new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build()), - /* track= */ 0), - new FixedTrackSelection( + /* selectedIndex= */ 0), + new FakeTrackSelection( new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build()), - /* track= */ 0) + /* selectedIndex= */ 0) }; - TrackSelectorResult trackSelectorResult = + TrackSelectorResult preloadTrackSelectorResult = new TrackSelectorResult( new RendererConfiguration[] { RendererConfiguration.DEFAULT, RendererConfiguration.DEFAULT }, - trackSelections, + preloadTrackSelections, Tracks.EMPTY, /* info= */ null); - when(wrappedMediaPeriod.selectTracks(eq(trackSelections), any(), any(), any(), anyLong())) - .thenAnswer(invocation -> invocation.getArgument(4, Long.class)); + SampleStream[] preloadedStreams = + new SampleStream[] {new EmptySampleStream(), new EmptySampleStream()}; + when(wrappedMediaPeriod.selectTracks( + eq(preloadTrackSelections), any(), any(), any(), /* positionUs= */ eq(0L))) + .thenAnswer( + invocation -> { + boolean[] mayRetainStreamFlags = invocation.getArgument(1); + SampleStream[] streams = invocation.getArgument(2); + boolean[] streamResetFlags = invocation.getArgument(3); + for (int i = 0; i < streams.length; i++) { + if (!mayRetainStreamFlags[i]) { + streams[i] = preloadedStreams[i]; + streamResetFlags[i] = true; + } + } + return 0L; + }); PreloadMediaPeriod preloadMediaPeriod = new PreloadMediaPeriod(wrappedMediaPeriod); MediaPeriod.Callback callback = new MediaPeriod.Callback() { @@ -402,26 +1170,60 @@ public final class PreloadMediaPeriodTest { public void onContinueLoadingRequested(MediaPeriod source) {} }; preloadMediaPeriod.prepare(callback, /* positionUs= */ 0L); - // Select tracks for preloading. - preloadMediaPeriod.selectTracksForPreloading(trackSelectorResult, /* positionUs= */ 0L); + preloadMediaPeriod.selectTracksForPreloading(preloadTrackSelectorResult, /* positionUs= */ 0L); + verify(wrappedMediaPeriod).prepare(any(), anyLong()); verify(wrappedMediaPeriod) - .selectTracks(eq(trackSelections), any(), any(), any(), /* positionUs= */ eq(0L)); - SampleStream[] streams = new SampleStream[2]; - boolean[] streamResetFlags = new boolean[2]; + .selectTracks( + eq(preloadTrackSelections), + /* mayRetainStreamFlags= */ eq(new boolean[] {false, false}), + /* streams= */ any(), + /* streamResetFlags= */ any(), + /* positionUs= */ eq(0L)); + verifyNoMoreInteractions(wrappedMediaPeriod); + reset(wrappedMediaPeriod); + boolean[] reselectStreamResetFlags = new boolean[2]; + SampleStream[] newStreams = + new SampleStream[] {new EmptySampleStream(), new EmptySampleStream()}; + when(wrappedMediaPeriod.selectTracks( + eq(preloadTrackSelections), + /* mayRetainStreamFlags= */ eq(new boolean[] {false, false}), + /* streams= */ eq(preloadedStreams), + /* streamResetFlags= */ eq(reselectStreamResetFlags), + /* positionUs= */ eq(1234L))) + .thenAnswer( + invocation -> { + boolean[] mayRetainStreamFlags = invocation.getArgument(1); + SampleStream[] streams = invocation.getArgument(2); + boolean[] streamResetFlags = invocation.getArgument(3); + for (int i = 0; i < streams.length; i++) { + if (!mayRetainStreamFlags[i]) { + streams[i] = newStreams[i]; + streamResetFlags[i] = true; + } + } + return 1234L; + }); + SampleStream[] reselectStreams = new SampleStream[2]; + // Select tracks based on the same track selections but at a different position. long trackSelectionStartPositionUs = preloadMediaPeriod.selectTracks( - trackSelections, new boolean[2], streams, streamResetFlags, /* positionUs= */ 10L); + preloadTrackSelections, + new boolean[2], + reselectStreams, + reselectStreamResetFlags, + /* positionUs= */ 1234L); verify(wrappedMediaPeriod) .selectTracks( - eq(trackSelections), - any(), - same(streams), - same(streamResetFlags), - /* positionUs= */ eq(10L)); - assertThat(trackSelectionStartPositionUs).isEqualTo(10L); + eq(preloadTrackSelections), + /* mayRetainStreamFlags= */ eq(new boolean[] {false, false}), + /* streams= */ eq(newStreams), + /* streamResetFlags= */ same(reselectStreamResetFlags), + /* positionUs= */ eq(1234L)); + verifyNoMoreInteractions(wrappedMediaPeriod); + assertThat(trackSelectionStartPositionUs).isEqualTo(1234L); } @Test diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTrackSelection.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTrackSelection.java index 59c28df0f9..e00593de85 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTrackSelection.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTrackSelection.java @@ -25,6 +25,7 @@ import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.source.chunk.MediaChunk; import androidx.media3.exoplayer.source.chunk.MediaChunkIterator; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; +import com.google.common.base.Objects; import java.util.List; /** @@ -35,13 +36,19 @@ import java.util.List; public final class FakeTrackSelection implements ExoTrackSelection { private final TrackGroup rendererTrackGroup; + private final int selectedIndex; public int enableCount; public int releaseCount; public boolean isEnabled; public FakeTrackSelection(TrackGroup rendererTrackGroup) { + this(rendererTrackGroup, /* selectedIndex= */ 0); + } + + public FakeTrackSelection(TrackGroup rendererTrackGroup, int selectedIndex) { this.rendererTrackGroup = rendererTrackGroup; + this.selectedIndex = selectedIndex; } // TrackSelection implementation. @@ -68,18 +75,23 @@ public final class FakeTrackSelection implements ExoTrackSelection { @Override public int getIndexInTrackGroup(int index) { - return 0; + return index; } @Override public int indexOf(Format format) { assertThat(isEnabled).isTrue(); - return 0; + for (int i = 0; i < rendererTrackGroup.length; i++) { + if (rendererTrackGroup.getFormat(i).equals(format)) { + return i; + } + } + return -1; } @Override public int indexOf(int indexInTrackGroup) { - return 0; + return indexInTrackGroup; } // ExoTrackSelection specific methods. @@ -102,17 +114,17 @@ public final class FakeTrackSelection implements ExoTrackSelection { @Override public Format getSelectedFormat() { - return rendererTrackGroup.getFormat(0); + return rendererTrackGroup.getFormat(selectedIndex); } @Override public int getSelectedIndexInTrackGroup() { - return 0; + return selectedIndex; } @Override public int getSelectedIndex() { - return 0; + return selectedIndex; } @Override @@ -158,4 +170,26 @@ public final class FakeTrackSelection implements ExoTrackSelection { assertThat(isEnabled).isTrue(); return false; } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FakeTrackSelection)) { + return false; + } + FakeTrackSelection that = (FakeTrackSelection) o; + return enableCount == that.enableCount + && releaseCount == that.releaseCount + && isEnabled == that.isEnabled + && selectedIndex == that.selectedIndex + && Objects.equal(rendererTrackGroup, that.rendererTrackGroup); + } + + @Override + public int hashCode() { + return Objects.hashCode( + rendererTrackGroup, enableCount, releaseCount, isEnabled, selectedIndex); + } }