Skip to content

Commit 2d8ae23

Browse files
icbakercopybara-github
authored andcommitted
Add fractional seek tolerance option to scrubbing mode
PiperOrigin-RevId: 756898758
1 parent 92a4fb4 commit 2d8ae23

File tree

4 files changed

+146
-7
lines changed

4 files changed

+146
-7
lines changed

RELEASENOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
scrubber bar around). The behavior of scrubbing mode can be customized
1010
with `setScrubbingModeParameters(..)` on `ExoPlayer` and
1111
`ExoPlayer.Builder`.
12+
* Allow customizing fractional seek tolerance in scrubbing mode.
1213
* Fix bug where prepare errors in the content of `AdsMediaSource` may be
1314
never reported ([#2337](https://github.com/androidx/media/issues/2337)).
1415
* Fix memory leak in `MergingMediaSource`, for example used when

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@
7676
import androidx.media3.exoplayer.upstream.BandwidthMeter;
7777
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
7878
import com.google.common.collect.ImmutableList;
79+
import com.google.common.math.DoubleMath;
7980
import java.io.IOException;
81+
import java.math.RoundingMode;
8082
import java.util.ArrayList;
8183
import java.util.Collections;
8284
import java.util.List;
@@ -223,6 +225,7 @@ public interface PlaybackInfoUpdateListener {
223225
private final AudioFocusManager audioFocusManager;
224226
private SeekParameters seekParameters;
225227
private ScrubbingModeParameters scrubbingModeParameters;
228+
@Nullable private SeekParameters scrubbingModeSeekParameters;
226229
private boolean scrubbingModeEnabled;
227230
private boolean seekIsPendingWhileScrubbing;
228231
@Nullable private SeekPosition queuedSeekWhileScrubbing;
@@ -1601,7 +1604,7 @@ private void seekToInternal(SeekPosition seekPosition, boolean incrementAcks)
16011604
&& newPeriodPositionUs != 0) {
16021605
newPeriodPositionUs =
16031606
playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs(
1604-
newPeriodPositionUs, seekParameters);
1607+
newPeriodPositionUs, getSeekParameters(window.durationUs));
16051608
}
16061609
if (Util.usToMs(newPeriodPositionUs) == Util.usToMs(playbackInfo.positionUs)
16071610
&& (playbackInfo.playbackState == Player.STATE_BUFFERING
@@ -1639,6 +1642,27 @@ private void seekToInternal(SeekPosition seekPosition, boolean incrementAcks)
16391642
}
16401643
}
16411644

1645+
private SeekParameters getSeekParameters(long durationUs) {
1646+
if (!scrubbingModeEnabled
1647+
|| durationUs == C.TIME_UNSET
1648+
|| scrubbingModeParameters.fractionalSeekToleranceBefore == null
1649+
|| scrubbingModeParameters.fractionalSeekToleranceAfter == null) {
1650+
return seekParameters;
1651+
}
1652+
long toleranceBeforeUs =
1653+
DoubleMath.roundToLong(
1654+
scrubbingModeParameters.fractionalSeekToleranceBefore * durationUs, RoundingMode.FLOOR);
1655+
long toleranceAfterUs =
1656+
DoubleMath.roundToLong(
1657+
scrubbingModeParameters.fractionalSeekToleranceAfter * durationUs, RoundingMode.FLOOR);
1658+
if (scrubbingModeSeekParameters == null
1659+
|| scrubbingModeSeekParameters.toleranceBeforeUs != toleranceBeforeUs
1660+
|| scrubbingModeSeekParameters.toleranceAfterUs != toleranceAfterUs) {
1661+
scrubbingModeSeekParameters = new SeekParameters(toleranceBeforeUs, toleranceAfterUs);
1662+
}
1663+
return scrubbingModeSeekParameters;
1664+
}
1665+
16421666
private long seekToPeriodPosition(
16431667
MediaPeriodId periodId, long periodPositionUs, boolean forceBufferingState)
16441668
throws ExoPlaybackException {
@@ -1744,8 +1768,6 @@ private void setSeekParametersInternal(SeekParameters seekParameters) {
17441768

17451769
private void setScrubbingModeEnabledInternal(boolean scrubbingModeEnabled)
17461770
throws ExoPlaybackException {
1747-
this.scrubbingModeEnabled = scrubbingModeEnabled;
1748-
applyScrubbingModeParameters();
17491771
if (!scrubbingModeEnabled) {
17501772
seekIsPendingWhileScrubbing = false;
17511773
handler.removeMessages(MSG_SEEK_COMPLETED_IN_SCRUBBING_MODE);
@@ -1755,6 +1777,8 @@ private void setScrubbingModeEnabledInternal(boolean scrubbingModeEnabled)
17551777
queuedSeekWhileScrubbing = null;
17561778
}
17571779
}
1780+
this.scrubbingModeEnabled = scrubbingModeEnabled;
1781+
applyScrubbingModeParameters();
17581782
}
17591783

17601784
private void setScrubbingModeParametersInternal(ScrubbingModeParameters scrubbingModeParameters)

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ScrubbingModeParameters.java

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@
1515
*/
1616
package androidx.media3.exoplayer;
1717

18+
import static androidx.media3.common.util.Assertions.checkArgument;
19+
20+
import androidx.annotation.FloatRange;
1821
import androidx.annotation.Nullable;
1922
import androidx.media3.common.C;
2023
import androidx.media3.common.C.TrackType;
2124
import androidx.media3.common.util.UnstableApi;
2225
import com.google.common.collect.ImmutableSet;
2326
import com.google.errorprone.annotations.CanIgnoreReturnValue;
27+
import java.util.Objects;
2428
import java.util.Set;
2529

2630
/**
@@ -42,6 +46,8 @@ public final class ScrubbingModeParameters {
4246
*/
4347
public static final class Builder {
4448
private ImmutableSet<@TrackType Integer> disabledTrackTypes;
49+
@Nullable private Double fractionalSeekToleranceBefore;
50+
@Nullable private Double fractionalSeekToleranceAfter;
4551

4652
/** Creates an instance. */
4753
public Builder() {
@@ -50,6 +56,8 @@ public Builder() {
5056

5157
private Builder(ScrubbingModeParameters scrubbingModeParameters) {
5258
this.disabledTrackTypes = scrubbingModeParameters.disabledTrackTypes;
59+
this.fractionalSeekToleranceBefore = scrubbingModeParameters.fractionalSeekToleranceBefore;
60+
this.fractionalSeekToleranceAfter = scrubbingModeParameters.fractionalSeekToleranceAfter;
5361
}
5462

5563
/**
@@ -69,6 +77,37 @@ public Builder setDisabledTrackTypes(Set<@TrackType Integer> disabledTrackTypes)
6977
return this;
7078
}
7179

80+
/**
81+
* Sets the fraction of the media duration to use for {@link SeekParameters#toleranceBeforeUs}
82+
* and {@link SeekParameters#toleranceAfterUs} when scrubbing.
83+
*
84+
*

Pass {@code null} for both values to use the {@linkplain ExoPlayer#getSeekParameters()

85+
* player-level seek parameters} when scrubbing.
86+
*
87+
*

Defaults to {code null} for both values, so all seeks are exact (this may change in a

88+
* future release).
89+
*
90+
*

See {@link ScrubbingModeParameters#fractionalSeekToleranceBefore} and {@link

91+
* ScrubbingModeParameters#fractionalSeekToleranceAfter}.
92+
*
93+
* @param toleranceBefore The fraction of the media duration to use for {@link
94+
* SeekParameters#toleranceBeforeUs}, or null to use the player-level seek parameters.
95+
* @param toleranceAfter The fraction of the media duration to use for {@link
96+
* SeekParameters#toleranceAfterUs}, or null to use the player-level seek parameters.
97+
* @return This builder for convenience.
98+
*/
99+
@CanIgnoreReturnValue
100+
public Builder setFractionalSeekTolerance(
101+
@Nullable @FloatRange(from = 0, to = 1) Double toleranceBefore,
102+
@Nullable @FloatRange(from = 0, to = 1) Double toleranceAfter) {
103+
checkArgument((toleranceBefore == null) == (toleranceAfter == null));
104+
checkArgument(toleranceBefore == null || (toleranceBefore >= 0 && toleranceBefore <= 1));
105+
checkArgument(toleranceAfter == null || (toleranceAfter >= 0 && toleranceAfter <= 1));
106+
this.fractionalSeekToleranceBefore = toleranceBefore;
107+
this.fractionalSeekToleranceAfter = toleranceAfter;
108+
return this;
109+
}
110+
72111
/** Returns the built {@link ScrubbingModeParameters}. */
73112
public ScrubbingModeParameters build() {
74113
return new ScrubbingModeParameters(this);
@@ -78,8 +117,32 @@ public ScrubbingModeParameters build() {
78117
/** Which track types will be disabled in scrubbing mode. */
79118
public final ImmutableSet<@TrackType Integer> disabledTrackTypes;
80119

120+
/**
121+
* The fraction of the media duration to use for {@link SeekParameters#toleranceBeforeUs} when
122+
* scrubbing.
123+
*
124+
*

If this is {@code null} or the media duration is not known then the {@linkplain

125+
* ExoPlayer#getSeekParameters()} non-scrubbing seek parameters} are used.
126+
*/
127+
@Nullable
128+
@FloatRange(from = 0, to = 1)
129+
public final Double fractionalSeekToleranceBefore;
130+
131+
/**
132+
* The fraction of the media duration to use for {@link SeekParameters#toleranceAfterUs} when
133+
* scrubbing.
134+
*
135+
*

If this is {@code null} or the media duration is not known then the {@linkplain

136+
* ExoPlayer#getSeekParameters()} non-scrubbing seek parameters} are used.
137+
*/
138+
@Nullable
139+
@FloatRange(from = 0, to = 1)
140+
public final Double fractionalSeekToleranceAfter;
141+
81142
private ScrubbingModeParameters(Builder builder) {
82143
this.disabledTrackTypes = builder.disabledTrackTypes;
144+
this.fractionalSeekToleranceBefore = builder.fractionalSeekToleranceBefore;
145+
this.fractionalSeekToleranceAfter = builder.fractionalSeekToleranceAfter;
83146
}
84147

85148
/** Returns a {@link Builder} initialized with the values from this instance. */
@@ -93,11 +156,14 @@ public boolean equals(@Nullable Object o) {
93156
return false;
94157
}
95158
ScrubbingModeParameters that = (ScrubbingModeParameters) o;
96-
return disabledTrackTypes.equals(that.disabledTrackTypes);
159+
return disabledTrackTypes.equals(that.disabledTrackTypes)
160+
&& Objects.equals(fractionalSeekToleranceBefore, that.fractionalSeekToleranceBefore)
161+
&& Objects.equals(fractionalSeekToleranceAfter, that.fractionalSeekToleranceAfter);
97162
}
98163

99164
@Override
100165
public int hashCode() {
101-
return disabledTrackTypes.hashCode();
166+
return Objects.hash(
167+
disabledTrackTypes, fractionalSeekToleranceBefore, fractionalSeekToleranceAfter);
102168
}
103169
}

libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerScrubbingTest.java

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ public void scrubbingMode_disablesAudioTrack_masksTrackSelectionParameters() thr
349349
}
350350

351351
@Test
352-
public void customizeParameters_beforeScrubbingModeEnabled() throws Exception {
352+
public void customizeDisabledTracks_beforeScrubbingModeEnabled() throws Exception {
353353
Timeline timeline = new FakeTimeline();
354354
ExoPlayer player =
355355
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build();
@@ -381,7 +381,7 @@ public void customizeParameters_beforeScrubbingModeEnabled() throws Exception {
381381
}
382382

383383
@Test
384-
public void customizeParameters_duringScrubbingMode() throws Exception {
384+
public void customizeDisabledTracks_duringScrubbingMode() throws Exception {
385385
Timeline timeline = new FakeTimeline();
386386
ExoPlayer player =
387387
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build();
@@ -412,4 +412,52 @@ public void customizeParameters_duringScrubbingMode() throws Exception {
412412
player.release();
413413
surface.release();
414414
}
415+
416+
@Test
417+
public void fractionalSeekTolerance_isPropagated() throws Exception {
418+
Timeline timeline =
419+
new FakeTimeline(
420+
new TimelineWindowDefinition.Builder().setWindowPositionInFirstPeriodUs(0).build());
421+
ExoPlayer player =
422+
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build();
423+
Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1));
424+
player.setVideoSurface(surface);
425+
Player.Listener mockListener = mock(Player.Listener.class);
426+
player.addListener(mockListener);
427+
player.setScrubbingModeParameters(
428+
new ScrubbingModeParameters.Builder()
429+
.setFractionalSeekTolerance(/* toleranceBefore= */ 0.1, /* toleranceAfter= */ 0.1)
430+
.build());
431+
player.setMediaSource(
432+
new FakeMediaSource.Builder()
433+
.setTimeline(timeline)
434+
.setTrackDataFactory(
435+
TrackDataFactory.samplesWithRateDurationAndKeyframeInterval(
436+
/* initialSampleTimeUs= */ 0,
437+
/* sampleRate= */ 30,
438+
/* durationUs= */ DEFAULT_WINDOW_DURATION_US,
439+
/* keyFrameInterval= */ 60))
440+
.setSyncSampleTimesUs(new long[] {0, 2_000_000, 4_000_000, 6_000_000, 8_000_000})
441+
.setFormats(ExoPlayerTestRunner.VIDEO_FORMAT)
442+
.build());
443+
player.prepare();
444+
player.play();
445+
advance(player).untilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 1000);
446+
VideoFrameMetadataListener mockVideoFrameMetadataListener =
447+
mock(VideoFrameMetadataListener.class);
448+
player.setVideoFrameMetadataListener(mockVideoFrameMetadataListener);
449+
450+
player.setScrubbingModeEnabled(true);
451+
advance(player).untilPendingCommandsAreFullyHandled();
452+
player.seekTo(2500);
453+
advance(player).untilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 2000);
454+
player.setScrubbingModeEnabled(false);
455+
player.clearVideoFrameMetadataListener(mockVideoFrameMetadataListener);
456+
advance(player).untilState(Player.STATE_ENDED);
457+
player.release();
458+
surface.release();
459+
460+
verify(mockVideoFrameMetadataListener)
461+
.onVideoFrameAboutToBeRendered(eq(2_000_000L), anyLong(), any(), any());
462+
}
415463
}

0 commit comments

Comments
 (0)