mirror of
https://github.com/androidx/media.git
synced 2025-04-29 22:36:54 +08:00
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:
parent
6c4c4bdea4
commit
83efd8eb66
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user