diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5e47e3c870..1557f33dd3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,7 +14,9 @@ ([#2229](https://github.com/androidx/media/issues/2229)). * Add `ExoPlayer.setScrubbingModeEnabled(boolean)` method. This optimizes the player for many frequent seeks (for example, from a user dragging a - scrubber bar around). + scrubber bar around). The behavior of scrubbing mode can be customized + with `setScrubbingModeParameters(..)` on `ExoPlayer` and + `ExoPlayer.Builder`. * `AdPlaybackState.withAdDurationsUs(long[][])` can be used after ad groups have been removed. The user still needs to pass in an array of durations for removed ad groups which can be empty or null diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java index 31642affa7..cb3ea12a8f 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java +++ b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java @@ -849,10 +849,8 @@ public class TrackSelectionParameters { * * @param disabledTrackTypes The track types to disable. * @return This builder. - * @deprecated Use {@link #setTrackTypeDisabled(int, boolean)}. */ @CanIgnoreReturnValue - @Deprecated @UnstableApi public Builder setDisabledTrackTypes(Set<@C.TrackType Integer> disabledTrackTypes) { this.disabledTrackTypes.clear(); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java index c7fbd1c05b..9543742d8b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -238,6 +238,7 @@ public interface ExoPlayer extends Player { @C.VideoChangeFrameRateStrategy /* package */ int videoChangeFrameRateStrategy; /* package */ boolean useLazyPreparation; /* package */ SeekParameters seekParameters; + /* package */ ScrubbingModeParameters scrubbingModeParameters; /* package */ long seekBackIncrementMs; /* package */ long seekForwardIncrementMs; /* package */ long maxSeekToPreviousPositionMs; @@ -452,6 +453,7 @@ public interface ExoPlayer extends Player { seekBackIncrementMs = C.DEFAULT_SEEK_BACK_INCREMENT_MS; seekForwardIncrementMs = C.DEFAULT_SEEK_FORWARD_INCREMENT_MS; maxSeekToPreviousPositionMs = C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS; + scrubbingModeParameters = ScrubbingModeParameters.DEFAULT; livePlaybackSpeedControl = new DefaultLivePlaybackSpeedControl.Builder().build(); clock = Clock.DEFAULT; releaseTimeoutMs = DEFAULT_RELEASE_TIMEOUT_MS; @@ -897,6 +899,22 @@ public interface ExoPlayer extends Player { return this; } + /** + * Sets the parameters that control the behavior in {@linkplain #setScrubbingModeEnabled + * scrubbing mode}. + * + * @param scrubbingModeParameters The {@link ScrubbingModeParameters}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + @CanIgnoreReturnValue + @UnstableApi + public Builder setScrubbingModeParameters(ScrubbingModeParameters scrubbingModeParameters) { + checkState(!buildCalled); + this.scrubbingModeParameters = checkNotNull(scrubbingModeParameters); + return this; + } + /** * Sets a timeout for calls to {@link #release} and {@link #setForegroundMode}. * @@ -1466,6 +1484,20 @@ public interface ExoPlayer extends Player { @UnstableApi void setScrubbingModeEnabled(boolean scrubbingModeEnabled); + /** + * Sets the parameters that control behavior in {@linkplain #setScrubbingModeEnabled scrubbing + * mode}. + */ + @UnstableApi + void setScrubbingModeParameters(ScrubbingModeParameters scrubbingModeParameters); + + /** + * Gets the parameters that control behavior in {@linkplain #setScrubbingModeEnabled scrubbing + * mode}. + */ + @UnstableApi + ScrubbingModeParameters getScrubbingModeParameters(); + /** * Sets a {@link List} of {@linkplain Effect video effects} that will be applied to each video * frame. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index c5da5ad1f6..19ff65ceb1 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -58,6 +58,7 @@ import androidx.media3.common.AudioAttributes; import androidx.media3.common.AuxEffectInfo; import androidx.media3.common.BasePlayer; import androidx.media3.common.C; +import androidx.media3.common.C.TrackType; import androidx.media3.common.DeviceInfo; import androidx.media3.common.Effect; import androidx.media3.common.Format; @@ -116,6 +117,7 @@ import androidx.media3.exoplayer.video.VideoRendererEventListener; import androidx.media3.exoplayer.video.spherical.CameraMotionListener; import androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -182,6 +184,8 @@ import java.util.concurrent.CopyOnWriteArraySet; private boolean pendingDiscontinuity; private boolean foregroundMode; private boolean scrubbingModeEnabled; + @Nullable private ImmutableSet<@TrackType Integer> disabledTrackTypesWithoutScrubbingMode; + private ScrubbingModeParameters scrubbingModeParameters; private SeekParameters seekParameters; private ShuffleOrder shuffleOrder; private PreloadConfiguration preloadConfiguration; @@ -284,6 +288,7 @@ import java.util.concurrent.CopyOnWriteArraySet; this.seekBackIncrementMs = builder.seekBackIncrementMs; this.seekForwardIncrementMs = builder.seekForwardIncrementMs; this.maxSeekToPreviousPositionMs = builder.maxSeekToPreviousPositionMs; + this.scrubbingModeParameters = builder.scrubbingModeParameters; this.pauseAtEndOfMediaItems = builder.pauseAtEndOfMediaItems; this.applicationLooper = builder.looper; this.clock = builder.clock; @@ -1224,20 +1229,38 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public TrackSelectionParameters getTrackSelectionParameters() { verifyApplicationThread(); - return trackSelector.getParameters(); + TrackSelectionParameters parameters = trackSelector.getParameters(); + return scrubbingModeEnabled + ? parameters + .buildUpon() + .setDisabledTrackTypes(disabledTrackTypesWithoutScrubbingMode) + .build() + : parameters; } @Override public void setTrackSelectionParameters(TrackSelectionParameters parameters) { verifyApplicationThread(); - if (!trackSelector.isSetParametersSupported() - || parameters.equals(trackSelector.getParameters())) { + if (!trackSelector.isSetParametersSupported()) { return; } - trackSelector.setParameters(parameters); - listeners.sendEvent( - EVENT_TRACK_SELECTION_PARAMETERS_CHANGED, - listener -> listener.onTrackSelectionParametersChanged(parameters)); + TrackSelectionParameters publicParametersBeforeUpdate = getTrackSelectionParameters(); + TrackSelectionParameters internalParameters; + if (scrubbingModeEnabled) { + this.disabledTrackTypesWithoutScrubbingMode = parameters.disabledTrackTypes; + internalParameters = + addDisabledTrackTypes(parameters, scrubbingModeParameters.disabledTrackTypes); + } else { + internalParameters = parameters; + } + if (!internalParameters.equals(trackSelector.getParameters())) { + trackSelector.setParameters(internalParameters); + } + if (!publicParametersBeforeUpdate.equals(parameters)) { + listeners.sendEvent( + EVENT_TRACK_SELECTION_PARAMETERS_CHANGED, + listener -> listener.onTrackSelectionParametersChanged(parameters)); + } } @Override @@ -1559,10 +1582,64 @@ import java.util.concurrent.CopyOnWriteArraySet; return; } this.scrubbingModeEnabled = scrubbingModeEnabled; + if (!scrubbingModeParameters.disabledTrackTypes.isEmpty() + && trackSelector.isSetParametersSupported()) { + TrackSelectionParameters previousTrackSelectionParameters = trackSelector.getParameters(); + TrackSelectionParameters newTrackSelectionParameters; + if (scrubbingModeEnabled) { + this.disabledTrackTypesWithoutScrubbingMode = + previousTrackSelectionParameters.disabledTrackTypes; + newTrackSelectionParameters = + addDisabledTrackTypes( + previousTrackSelectionParameters, scrubbingModeParameters.disabledTrackTypes); + } else { + newTrackSelectionParameters = + previousTrackSelectionParameters + .buildUpon() + .setDisabledTrackTypes(disabledTrackTypesWithoutScrubbingMode) + .build(); + this.disabledTrackTypesWithoutScrubbingMode = null; + } + // Set the parameters directly, to avoid firing onTrackSelectionParametersChanged (because + // the return value of getTrackSelectionParameters won't change). + if (!newTrackSelectionParameters.equals(previousTrackSelectionParameters)) { + trackSelector.setParameters(newTrackSelectionParameters); + } + } internalPlayer.setScrubbingModeEnabled(scrubbingModeEnabled); maybeUpdatePlaybackSuppressionReason(); } + @Override + public void setScrubbingModeParameters(ScrubbingModeParameters scrubbingModeParameters) { + verifyApplicationThread(); + if (this.scrubbingModeParameters.equals(scrubbingModeParameters)) { + return; + } + ScrubbingModeParameters previousParameters = this.scrubbingModeParameters; + this.scrubbingModeParameters = scrubbingModeParameters; + if (scrubbingModeEnabled + && trackSelector.isSetParametersSupported() + && !previousParameters.disabledTrackTypes.equals( + scrubbingModeParameters.disabledTrackTypes)) { + // We are already in scrubbing mode, but the tracks we should disable while scrubbing have + // changed, so we need to re-calculate the track selection parameters. + TrackSelectionParameters trackSelectionParameters = + addDisabledTrackTypes( + getTrackSelectionParameters(), scrubbingModeParameters.disabledTrackTypes); + if (!trackSelectionParameters.equals(trackSelector.getParameters())) { + // Set the parameters directly, to avoid firing onTrackSelectionParametersChanged. + trackSelector.setParameters(trackSelectionParameters); + } + } + } + + @Override + public ScrubbingModeParameters getScrubbingModeParameters() { + verifyApplicationThread(); + return scrubbingModeParameters; + } + @Override public AnalyticsCollector getAnalyticsCollector() { verifyApplicationThread(); @@ -2927,6 +3004,15 @@ import java.util.concurrent.CopyOnWriteArraySet; .build(); } + private static TrackSelectionParameters addDisabledTrackTypes( + TrackSelectionParameters parameters, ImmutableSet<@C.TrackType Integer> trackTypesToDisable) { + TrackSelectionParameters.Builder parametersBuilder = parameters.buildUpon(); + for (@C.TrackType Integer trackType : trackTypesToDisable) { + parametersBuilder.setTrackTypeDisabled(trackType, true); + } + return parametersBuilder.build(); + } + private static final class MediaSourceHolderSnapshot implements MediaSourceInfoHolder { private final Object uid; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ScrubbingModeParameters.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ScrubbingModeParameters.java new file mode 100644 index 0000000000..54e70ad554 --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ScrubbingModeParameters.java @@ -0,0 +1,103 @@ +/* + * Copyright 2025 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 + * + * http://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.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.C.TrackType; +import androidx.media3.common.util.UnstableApi; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.util.Set; + +/** + * Parameters to control the behavior of {@linkplain ExoPlayer#setScrubbingModeEnabled scrubbing + * mode}. + */ +@UnstableApi +public final class ScrubbingModeParameters { + + /** An instance which defines sensible default values for many scrubbing use-cases. */ + public static final ScrubbingModeParameters DEFAULT = + new ScrubbingModeParameters.Builder().build(); + + /** + * Builder for {@link ScrubbingModeParameters} instances. + * + *

This builder defines some defaults that may change in future releases of the library, and + * new properties may be added that default to enabled. + */ + public static final class Builder { + private ImmutableSet<@TrackType Integer> disabledTrackTypes; + + /** Creates an instance. */ + public Builder() { + this.disabledTrackTypes = ImmutableSet.of(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_METADATA); + } + + private Builder(ScrubbingModeParameters scrubbingModeParameters) { + this.disabledTrackTypes = scrubbingModeParameters.disabledTrackTypes; + } + + /** + * Sets which track types should be disabled in scrubbing mode. + * + *

Defaults to {@link C#TRACK_TYPE_AUDIO} and {@link C#TRACK_TYPE_METADATA} (this may change + * in a future release). + * + *

See {@link ScrubbingModeParameters#disabledTrackTypes}. + * + * @param disabledTrackTypes The track types to disable in scrubbing mode. + * @return This builder for convenience. + */ + @CanIgnoreReturnValue + public Builder setDisabledTrackTypes(Set<@TrackType Integer> disabledTrackTypes) { + this.disabledTrackTypes = ImmutableSet.copyOf(disabledTrackTypes); + return this; + } + + /** Returns the built {@link ScrubbingModeParameters}. */ + public ScrubbingModeParameters build() { + return new ScrubbingModeParameters(this); + } + } + + /** Which track types will be disabled in scrubbing mode. */ + public final ImmutableSet<@TrackType Integer> disabledTrackTypes; + + private ScrubbingModeParameters(Builder builder) { + this.disabledTrackTypes = builder.disabledTrackTypes; + } + + /** Returns a {@link Builder} initialized with the values from this instance. */ + public Builder buildUpon() { + return new Builder(this); + } + + @Override + public boolean equals(@Nullable Object o) { + if (!(o instanceof ScrubbingModeParameters)) { + return false; + } + ScrubbingModeParameters that = (ScrubbingModeParameters) o; + return disabledTrackTypes.equals(that.disabledTrackTypes); + } + + @Override + public int hashCode() { + return disabledTrackTypes.hashCode(); + } +} diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java index 60e4129f3a..7a9723c1ea 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java @@ -628,6 +628,18 @@ public class SimpleExoPlayer extends BasePlayer implements ExoPlayer { player.setScrubbingModeEnabled(scrubbingModeEnabled); } + @Override + public void setScrubbingModeParameters(ScrubbingModeParameters scrubbingModeParameters) { + blockUntilConstructorFinished(); + player.setScrubbingModeParameters(scrubbingModeParameters); + } + + @Override + public ScrubbingModeParameters getScrubbingModeParameters() { + blockUntilConstructorFinished(); + return player.getScrubbingModeParameters(); + } + @Override public AnalyticsCollector getAnalyticsCollector() { blockUntilConstructorFinished(); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java index 55ae6f3751..847e06e6c8 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java @@ -668,12 +668,8 @@ public class DefaultTrackSelector extends MappingTrackSelector return this; } - /** - * @deprecated Use {@link #setTrackTypeDisabled(int, boolean)}. - */ @CanIgnoreReturnValue @Override - @Deprecated @SuppressWarnings("deprecation") // Intentionally returning deprecated type public ParametersBuilder setDisabledTrackTypes(Set<@C.TrackType Integer> disabledTrackTypes) { delegate.setDisabledTrackTypes(disabledTrackTypes); @@ -1527,13 +1523,8 @@ public class DefaultTrackSelector extends MappingTrackSelector return this; } - /** - * @deprecated Use {@link #setTrackTypeDisabled(int, boolean)}. - */ @CanIgnoreReturnValue @Override - @Deprecated - @SuppressWarnings("deprecation") public Builder setDisabledTrackTypes(Set<@C.TrackType Integer> disabledTrackTypes) { super.setDisabledTrackTypes(disabledTrackTypes); return this; diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerScrubbingTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerScrubbingTest.java index be97959057..58945602d4 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerScrubbingTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerScrubbingTest.java @@ -19,32 +19,45 @@ import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.D import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.advance; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.graphics.SurfaceTexture; import android.view.Surface; import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; import androidx.media3.common.Player; import androidx.media3.common.Player.PositionInfo; import androidx.media3.common.Timeline; +import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.util.HandlerWrapper; +import androidx.media3.exoplayer.analytics.AnalyticsListener; import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.video.VideoFrameMetadataListener; import androidx.media3.test.utils.ExoPlayerTestRunner; +import androidx.media3.test.utils.FakeAudioRenderer; import androidx.media3.test.utils.FakeMediaPeriod.TrackDataFactory; import androidx.media3.test.utils.FakeMediaSource; import androidx.media3.test.utils.FakeRenderer; import androidx.media3.test.utils.FakeTimeline; import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition; +import androidx.media3.test.utils.FakeVideoRenderer; import androidx.media3.test.utils.TestExoPlayerBuilder; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableSet; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; /** Tests for {@linkplain ExoPlayer#setScrubbingModeEnabled(boolean) scrubbing mode}. */ @RunWith(AndroidJUnit4.class) @@ -52,19 +65,14 @@ public final class ExoPlayerScrubbingTest { @Test public void scrubbingMode_suppressesPlayback() throws Exception { - Timeline timeline = new FakeTimeline(); - FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); ExoPlayer player = - new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) - .setRenderers(renderer) - .build(); + new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build(); Player.Listener mockListener = mock(Player.Listener.class); player.addListener(mockListener); - - player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT)); + player.setMediaSource( + new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT)); player.prepare(); player.play(); - advance(player).untilPosition(0, 2000); player.setScrubbingModeEnabled(true); @@ -89,7 +97,6 @@ public final class ExoPlayerScrubbingTest { player.setVideoSurface(surface); Player.Listener mockListener = mock(Player.Listener.class); player.addListener(mockListener); - player.setMediaSource( new FakeMediaSource( timeline, @@ -102,12 +109,11 @@ public final class ExoPlayerScrubbingTest { ExoPlayerTestRunner.VIDEO_FORMAT)); player.prepare(); player.play(); - advance(player).untilPosition(0, 1000); - VideoFrameMetadataListener mockVideoFrameMetadataListener = mock(VideoFrameMetadataListener.class); player.setVideoFrameMetadataListener(mockVideoFrameMetadataListener); + player.setScrubbingModeEnabled(true); advance(player).untilPendingCommandsAreFullyHandled(); player.seekTo(2500); @@ -123,7 +129,6 @@ public final class ExoPlayerScrubbingTest { player.setScrubbingModeEnabled(false); advance(player).untilPendingCommandsAreFullyHandled(); player.clearVideoFrameMetadataListener(mockVideoFrameMetadataListener); - advance(player).untilState(Player.STATE_ENDED); player.release(); surface.release(); @@ -131,7 +136,6 @@ public final class ExoPlayerScrubbingTest { ArgumentCaptor presentationTimeUsCaptor = ArgumentCaptor.forClass(Long.class); verify(mockVideoFrameMetadataListener, atLeastOnce()) .onVideoFrameAboutToBeRendered(presentationTimeUsCaptor.capture(), anyLong(), any(), any()); - assertThat(presentationTimeUsCaptor.getAllValues()) .containsExactly(2_500_000L, 3_500_000L, 4_500_000L) .inOrder(); @@ -148,4 +152,249 @@ public final class ExoPlayerScrubbingTest { .containsExactly(2500L, 3000L, 3500L, 4000L, 4500L) .inOrder(); } + + @Test + public void scrubbingMode_disablesAudioTrack_masksTrackSelectionParameters() throws Exception { + Timeline timeline = new FakeTimeline(); + ExoPlayer player = + new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build(); + Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1)); + player.setVideoSurface(surface); + player.setMediaSource( + new FakeMediaSource( + timeline, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT)); + player.prepare(); + player.play(); + advance(player).untilPosition(0, 1000); + TrackSelectionParameters trackSelectionParametersBeforeScrubbingMode = + player.getTrackSelectionParameters(); + AnalyticsListener mockAnalyticsListener = mock(AnalyticsListener.class); + player.addAnalyticsListener(mockAnalyticsListener); + + player.setScrubbingModeEnabled(true); + advance(player).untilPendingCommandsAreFullyHandled(); + verify(mockAnalyticsListener).onAudioDisabled(any(), any()); + verify(mockAnalyticsListener) + .onRendererReadyChanged(any(), anyInt(), eq(C.TRACK_TYPE_AUDIO), eq(false)); + verify(mockAnalyticsListener) + .onTracksChanged(any(), argThat(t -> !t.isTypeSelected(C.TRACK_TYPE_AUDIO))); + assertThat(player.getTrackSelectionParameters()) + .isEqualTo(trackSelectionParametersBeforeScrubbingMode); + + player.setScrubbingModeEnabled(false); + advance(player).untilPendingCommandsAreFullyHandled(); + verify(mockAnalyticsListener).onAudioEnabled(any(), any()); + verify(mockAnalyticsListener) + .onRendererReadyChanged(any(), anyInt(), eq(C.TRACK_TYPE_AUDIO), eq(true)); + verify(mockAnalyticsListener) + .onTracksChanged(any(), argThat(t -> t.isTypeSelected(C.TRACK_TYPE_AUDIO))); + assertThat(player.getTrackSelectionParameters()) + .isEqualTo(trackSelectionParametersBeforeScrubbingMode); + verify(mockAnalyticsListener, never()).onTrackSelectionParametersChanged(any(), any()); + + player.release(); + surface.release(); + } + + @Test + public void + disableTracksDuringScrubbingMode_typeThatIsDisabledByScrubbing_staysDisabledAfterScrubbing() + throws Exception { + Timeline timeline = new FakeTimeline(); + ExoPlayer player = + new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build(); + Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1)); + player.setVideoSurface(surface); + player.setMediaSource( + new FakeMediaSource( + timeline, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT)); + player.prepare(); + player.play(); + advance(player).untilPosition(0, 1000); + AnalyticsListener mockAnalyticsListener = mock(AnalyticsListener.class); + player.addAnalyticsListener(mockAnalyticsListener); + + player.setScrubbingModeEnabled(true); + advance(player).untilPendingCommandsAreFullyHandled(); + // Use InOrder so we can 'consume' verifications (see never() comment below). + InOrder analyticsListenerInOrder = inOrder(mockAnalyticsListener); + analyticsListenerInOrder.verify(mockAnalyticsListener).onAudioDisabled(any(), any()); + // Manually disable the audio track which is already temporarily disabled by scrubbing mode. + // This is a no-op until scrubbing mode ends, at which point the audio track should stay + // disabled. + TrackSelectionParameters newTrackSelectionParameters = + player + .getTrackSelectionParameters() + .buildUpon() + .setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, true) + .build(); + player.setTrackSelectionParameters(newTrackSelectionParameters); + assertThat(player.getTrackSelectionParameters()).isEqualTo(newTrackSelectionParameters); + analyticsListenerInOrder + .verify(mockAnalyticsListener) + .onTrackSelectionParametersChanged(any(), eq(newTrackSelectionParameters)); + + player.setScrubbingModeEnabled(false); + assertThat(player.getTrackSelectionParameters()).isEqualTo(newTrackSelectionParameters); + advance(player).untilPendingCommandsAreFullyHandled(); + // This is never() because the InOrder verification above already 'consumed' the + // expected onTrackSelectionParametersChanged call above, and we want to assert it's not fired + // again when we leave scrubbing mode. + analyticsListenerInOrder + .verify(mockAnalyticsListener, never()) + .onTrackSelectionParametersChanged(any(), any()); + analyticsListenerInOrder.verify(mockAnalyticsListener, never()).onAudioEnabled(any(), any()); + + player.release(); + surface.release(); + } + + @Test + public void + disableTracksDuringScrubbingMode_typeThatIsNotDisabledByScrubbing_immediatelyDisabled() + throws Exception { + Timeline timeline = new FakeTimeline(); + TestExoPlayerBuilder playerBuilder = + new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()); + ExoPlayer player = + playerBuilder + .setRenderersFactory( + (eventHandler, + videoRendererEventListener, + audioRendererEventListener, + textRendererOutput, + metadataRendererOutput) -> { + HandlerWrapper clockAwareHandler = + playerBuilder + .getClock() + .createHandler(eventHandler.getLooper(), /* callback= */ null); + return new Renderer[] { + new FakeVideoRenderer(clockAwareHandler, videoRendererEventListener), + new FakeAudioRenderer(clockAwareHandler, audioRendererEventListener), + new FakeRenderer(C.TRACK_TYPE_TEXT) + }; + }) + .build(); + Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1)); + player.setVideoSurface(surface); + Format textFormat = + new Format.Builder() + .setSampleMimeType(MimeTypes.TEXT_VTT) + .setLanguage("en") + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build(); + player.setMediaSource( + new FakeMediaSource( + timeline, + ExoPlayerTestRunner.VIDEO_FORMAT, + ExoPlayerTestRunner.AUDIO_FORMAT, + textFormat)); + player.prepare(); + player.play(); + advance(player).untilPosition(0, 1000); + AnalyticsListener mockAnalyticsListener = mock(AnalyticsListener.class); + player.addAnalyticsListener(mockAnalyticsListener); + + player.setScrubbingModeEnabled(true); + advance(player).untilPendingCommandsAreFullyHandled(); + // Use InOrder so we can 'consume' verifications (see never() comment below). + InOrder analyticsListenerInOrder = inOrder(mockAnalyticsListener); + verify(mockAnalyticsListener).onAudioDisabled(any(), any()); + // Manually disable the text track. This should be immediately disabled, and remain disabled + // after scrubbing mode ends. + TrackSelectionParameters newTrackSelectionParameters = + player + .getTrackSelectionParameters() + .buildUpon() + .setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true) + .build(); + player.setTrackSelectionParameters(newTrackSelectionParameters); + assertThat(player.getTrackSelectionParameters()).isEqualTo(newTrackSelectionParameters); + analyticsListenerInOrder + .verify(mockAnalyticsListener) + .onTrackSelectionParametersChanged(any(), eq(newTrackSelectionParameters)); + advance(player).untilPendingCommandsAreFullyHandled(); + analyticsListenerInOrder + .verify(mockAnalyticsListener) + .onRendererReadyChanged(any(), anyInt(), eq(C.TRACK_TYPE_TEXT), eq(false)); + + player.setScrubbingModeEnabled(false); + assertThat(player.getTrackSelectionParameters()).isEqualTo(newTrackSelectionParameters); + advance(player).untilPendingCommandsAreFullyHandled(); + // This is never() because the InOrder verification above already 'consumed' the + // expected onTrackSelectionParametersChanged call above, and we want to assert it's not fired + // again when we leave scrubbing mode. + analyticsListenerInOrder + .verify(mockAnalyticsListener, never()) + .onTrackSelectionParametersChanged(any(), any()); + analyticsListenerInOrder.verify(mockAnalyticsListener).onAudioEnabled(any(), any()); + + player.release(); + surface.release(); + } + + @Test + public void customizeParameters_beforeScrubbingModeEnabled() throws Exception { + Timeline timeline = new FakeTimeline(); + ExoPlayer player = + new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build(); + // Prevent any tracks being disabled during scrubbing + ScrubbingModeParameters scrubbingModeParameters = + new ScrubbingModeParameters.Builder().setDisabledTrackTypes(ImmutableSet.of()).build(); + player.setScrubbingModeParameters(scrubbingModeParameters); + assertThat(player.getScrubbingModeParameters()).isEqualTo(scrubbingModeParameters); + Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1)); + player.setVideoSurface(surface); + player.setMediaSource( + new FakeMediaSource( + timeline, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT)); + player.prepare(); + player.play(); + advance(player).untilPosition(0, 1000); + AnalyticsListener mockAnalyticsListener = mock(AnalyticsListener.class); + player.addAnalyticsListener(mockAnalyticsListener); + + player.setScrubbingModeEnabled(true); + advance(player).untilPendingCommandsAreFullyHandled(); + player.setScrubbingModeEnabled(false); + advance(player).untilPendingCommandsAreFullyHandled(); + + verify(mockAnalyticsListener, never()).onAudioDisabled(any(), any()); + + player.release(); + surface.release(); + } + + @Test + public void customizeParameters_duringScrubbingMode() throws Exception { + Timeline timeline = new FakeTimeline(); + ExoPlayer player = + new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build(); + Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1)); + player.setVideoSurface(surface); + player.setMediaSource( + new FakeMediaSource( + timeline, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT)); + player.prepare(); + player.play(); + advance(player).untilPosition(0, 1000); + AnalyticsListener mockAnalyticsListener = mock(AnalyticsListener.class); + player.addAnalyticsListener(mockAnalyticsListener); + + player.setScrubbingModeEnabled(true); + advance(player).untilPendingCommandsAreFullyHandled(); + + // Prevent any tracks being disabled during scrubbing + ScrubbingModeParameters scrubbingModeParameters = + new ScrubbingModeParameters.Builder().setDisabledTrackTypes(ImmutableSet.of()).build(); + player.setScrubbingModeParameters(scrubbingModeParameters); + + assertThat(player.getScrubbingModeParameters()).isEqualTo(scrubbingModeParameters); + advance(player).untilPendingCommandsAreFullyHandled(); + // Check that the audio track gets re-enabled, because the parameters changed to configure this. + verify(mockAnalyticsListener).onAudioEnabled(any(), any()); + + player.release(); + surface.release(); + } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ScrubbingModeParametersTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ScrubbingModeParametersTest.java new file mode 100644 index 0000000000..4d99c67e53 --- /dev/null +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ScrubbingModeParametersTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2025 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 + * + * http://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 com.google.common.truth.Truth.assertThat; + +import androidx.media3.common.C; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link ScrubbingModeParameters}. */ +@RunWith(AndroidJUnit4.class) +public final class ScrubbingModeParametersTest { + + @Test + public void defaultValues() { + assertThat(ScrubbingModeParameters.DEFAULT.disabledTrackTypes) + .containsExactly(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_METADATA); + } +} diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java index 0ef0431c1d..563add2a46 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java @@ -31,6 +31,7 @@ import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.PlayerMessage; import androidx.media3.exoplayer.Renderer; +import androidx.media3.exoplayer.ScrubbingModeParameters; import androidx.media3.exoplayer.SeekParameters; import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.AnalyticsListener; @@ -220,6 +221,16 @@ public class StubExoPlayer extends StubPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public void setScrubbingModeParameters(ScrubbingModeParameters scrubbingModeParameters) { + throw new UnsupportedOperationException(); + } + + @Override + public ScrubbingModeParameters getScrubbingModeParameters() { + throw new UnsupportedOperationException(); + } + @Override public void setVideoEffects(List videoEffects) { throw new UnsupportedOperationException();