Add ScrubbingModeParameters

This adds the option to disable certain track types during scrubbing,
with audio and metadata disabled by default.

The tracks are disabled by modifying the `TrackSelectionParameters`,
but in a way that is invisible to
`Player.getTrackSelectionParameters()` and
`Player.Listener.onTrackSelectionParametersChanged`. This allows us to
clearly reason about what should happen if
`Player.setTrackSelectionParameters(...)` is called during scrubbing
mode. The **side effects** of disabling the tracks are all visible
through `Player.Listener` and `AnalyticsListener` (renderer disabled,
decoder released, `onTracksChanged`, etc.).

PiperOrigin-RevId: 743961632
This commit is contained in:
ibaker 2025-04-04 08:56:43 -07:00 committed by Copybara-Service
parent 6c4c4bdea4
commit 83efd8eb66
10 changed files with 550 additions and 32 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
*
* <p>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.
*
* <p>Defaults to {@link C#TRACK_TYPE_AUDIO} and {@link C#TRACK_TYPE_METADATA} (this may change
* in a future release).
*
* <p>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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Effect> videoEffects) {
throw new UnsupportedOperationException();