diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9b548cf309..8541d0c730 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,7 @@ * Add `needsReconfiguration` API to the `MediaCodecAdapter` interface. * Update `MediaItem.Builder` javadoc to discourage calling setters that will be (currently) ignored if another setter is not also called. + * Add `fastForward` and `rewind` methods to `Player`. * Extractors: * Add support for MPEG-H 3D Audio in MP4 extractors ([#8860](https://github.com/google/ExoPlayer/pull/8860)). diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index a6e2eec14d..c5ecc30b0b 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.cast; +import static com.google.android.exoplayer2.util.Assertions.checkArgument; import static com.google.android.exoplayer2.util.Util.castNonNull; import static java.lang.Math.min; @@ -141,6 +142,8 @@ public final class CastPlayer extends BasePlayer { private int pendingSeekWindowIndex; private long pendingSeekPositionMs; @Nullable private PositionInfo pendingMediaItemRemovalPosition; + private long fastForwardIncrementMs; + private long rewindIncrementMs; /** * Creates a new cast player that uses a {@link DefaultMediaItemConverter}. @@ -178,6 +181,8 @@ public final class CastPlayer extends BasePlayer { availableCommands = new Commands.Builder().addAll(PERMANENT_AVAILABLE_COMMANDS).build(); pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekPositionMs = C.TIME_UNSET; + fastForwardIncrementMs = DEFAULT_FAST_FORWARD_INCREMENT_MS; + rewindIncrementMs = DEFAULT_REWIND_INCREMENT_MS; SessionManager sessionManager = castContext.getSessionManager(); sessionManager.addSessionManagerListener(statusListener, CastSession.class); @@ -411,6 +416,28 @@ public final class CastPlayer extends BasePlayer { listeners.flushEvents(); } + @Override + public void setFastForwardIncrement(long fastForwardIncrementMs) { + checkArgument(fastForwardIncrementMs > 0); + this.fastForwardIncrementMs = fastForwardIncrementMs; + } + + @Override + public long getFastForwardIncrement() { + return fastForwardIncrementMs; + } + + @Override + public void setRewindIncrement(long rewindIncrementMs) { + checkArgument(rewindIncrementMs > 0); + this.rewindIncrementMs = rewindIncrementMs; + } + + @Override + public long getRewindIncrement() { + return rewindIncrementMs; + } + @Override public void setPlaybackParameters(PlaybackParameters playbackParameters) { // Unsupported by the RemoteMediaClient API. Do nothing. diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java index 5eff45ee91..692ce2dc4d 100644 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java @@ -38,6 +38,8 @@ import static com.google.android.exoplayer2.Player.COMMAND_SET_SHUFFLE_MODE; import static com.google.android.exoplayer2.Player.COMMAND_SET_SPEED_AND_PITCH; import static com.google.android.exoplayer2.Player.COMMAND_SET_VIDEO_SURFACE; import static com.google.android.exoplayer2.Player.COMMAND_SET_VOLUME; +import static com.google.android.exoplayer2.Player.DEFAULT_FAST_FORWARD_INCREMENT_MS; +import static com.google.android.exoplayer2.Player.DEFAULT_REWIND_INCREMENT_MS; import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_REMOVE; import static com.google.android.exoplayer2.Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED; import static com.google.common.truth.Truth.assertThat; @@ -1102,6 +1104,98 @@ public class CastPlayerTest { inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt()); } + @Test + @SuppressWarnings("deprecation") // Mocks deprecated method used by the CastPlayer. + public void fastForward_notifiesPositionDiscontinuity() { + when(mockRemoteMediaClient.seek(anyLong())).thenReturn(mockPendingResult); + int[] mediaQueueItemIds = new int[] {1}; + List mediaItems = createMediaItems(mediaQueueItemIds); + int currentItemId = 1; + int[] streamTypes = new int[] {MediaInfo.STREAM_TYPE_BUFFERED}; + long[] durationsMs = new long[] {2 * DEFAULT_FAST_FORWARD_INCREMENT_MS}; + long positionMs = 0; + + castPlayer.addMediaItems(mediaItems); + updateTimeLine( + mediaItems, mediaQueueItemIds, currentItemId, streamTypes, durationsMs, positionMs); + castPlayer.fastForward(); + + Player.PositionInfo oldPosition = + new Player.PositionInfo( + /* windowUid= */ 1, + /* windowIndex= */ 0, + /* periodUid= */ 1, + /* periodIndex= */ 0, + /* positionMs= */ 0, + /* contentPositionMs= */ 0, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET); + Player.PositionInfo newPosition = + new Player.PositionInfo( + /* windowUid= */ 1, + /* windowIndex= */ 0, + /* periodUid= */ 1, + /* periodIndex= */ 0, + /* positionMs= */ DEFAULT_FAST_FORWARD_INCREMENT_MS, + /* contentPositionMs= */ DEFAULT_FAST_FORWARD_INCREMENT_MS, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET); + InOrder inOrder = Mockito.inOrder(mockListener); + inOrder.verify(mockListener).onPositionDiscontinuity(eq(Player.DISCONTINUITY_REASON_SEEK)); + inOrder + .verify(mockListener) + .onPositionDiscontinuity( + eq(oldPosition), eq(newPosition), eq(Player.DISCONTINUITY_REASON_SEEK)); + inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt()); + inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt()); + } + + @Test + @SuppressWarnings("deprecation") // Mocks deprecated method used by the CastPlayer. + public void rewind_notifiesPositionDiscontinuity() { + when(mockRemoteMediaClient.seek(anyLong())).thenReturn(mockPendingResult); + int[] mediaQueueItemIds = new int[] {1}; + List mediaItems = createMediaItems(mediaQueueItemIds); + int currentItemId = 1; + int[] streamTypes = new int[] {MediaInfo.STREAM_TYPE_BUFFERED}; + long[] durationsMs = new long[] {3 * DEFAULT_REWIND_INCREMENT_MS}; + long positionMs = 2 * DEFAULT_REWIND_INCREMENT_MS; + + castPlayer.addMediaItems(mediaItems); + updateTimeLine( + mediaItems, mediaQueueItemIds, currentItemId, streamTypes, durationsMs, positionMs); + castPlayer.rewind(); + + Player.PositionInfo oldPosition = + new Player.PositionInfo( + /* windowUid= */ 1, + /* windowIndex= */ 0, + /* periodUid= */ 1, + /* periodIndex= */ 0, + /* positionMs= */ 2 * DEFAULT_REWIND_INCREMENT_MS, + /* contentPositionMs= */ 2 * DEFAULT_REWIND_INCREMENT_MS, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET); + Player.PositionInfo newPosition = + new Player.PositionInfo( + /* windowUid= */ 1, + /* windowIndex= */ 0, + /* periodUid= */ 1, + /* periodIndex= */ 0, + /* positionMs= */ DEFAULT_REWIND_INCREMENT_MS, + /* contentPositionMs= */ DEFAULT_REWIND_INCREMENT_MS, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET); + InOrder inOrder = Mockito.inOrder(mockListener); + inOrder.verify(mockListener).onPositionDiscontinuity(eq(Player.DISCONTINUITY_REASON_SEEK)); + inOrder + .verify(mockListener) + .onPositionDiscontinuity( + eq(oldPosition), eq(newPosition), eq(Player.DISCONTINUITY_REASON_SEEK)); + inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt()); + inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt()); + } + @Test public void isCommandAvailable_isTrueForAvailableCommands() { int[] mediaQueueItemIds = new int[] {1, 2}; diff --git a/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java index 4ef055e9ad..18fe8c72f0 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2; +import static java.lang.Math.max; +import static java.lang.Math.min; + import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Util; import java.util.Collections; @@ -118,6 +121,16 @@ public abstract class BasePlayer implements Player { seekTo(getCurrentWindowIndex(), positionMs); } + @Override + public final void fastForward() { + seekToOffset(getFastForwardIncrement()); + } + + @Override + public final void rewind() { + seekToOffset(-getRewindIncrement()); + } + @Override public final boolean hasPrevious() { return getPreviousWindowIndex() != C.INDEX_UNSET; @@ -246,12 +259,6 @@ public abstract class BasePlayer implements Player { : timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); } - @RepeatMode - private int getRepeatModeForNavigation() { - @RepeatMode int repeatMode = getRepeatMode(); - return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode; - } - protected Commands getAvailableCommands(Commands permanentAvailableCommands) { return new Commands.Builder() .addAll(permanentAvailableCommands) @@ -262,4 +269,20 @@ public abstract class BasePlayer implements Player { .addIf(COMMAND_SEEK_TO_MEDIA_ITEM, !isPlayingAd()) .build(); } + + @RepeatMode + private int getRepeatModeForNavigation() { + @RepeatMode int repeatMode = getRepeatMode(); + return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode; + } + + private void seekToOffset(long offsetMs) { + long positionMs = getCurrentPosition() + offsetMs; + long durationMs = getDuration(); + if (durationMs != C.TIME_UNSET) { + positionMs = min(positionMs, durationMs); + } + positionMs = max(positionMs, 0); + seekTo(positionMs); + } } diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ForwardingPlayer.java b/library/common/src/main/java/com/google/android/exoplayer2/ForwardingPlayer.java index 2925987807..a687c734c7 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/ForwardingPlayer.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/ForwardingPlayer.java @@ -247,6 +247,36 @@ public class ForwardingPlayer implements Player { player.seekTo(windowIndex, positionMs); } + @Override + public void setFastForwardIncrement(long fastForwardIncrementMs) { + player.setFastForwardIncrement(fastForwardIncrementMs); + } + + @Override + public long getFastForwardIncrement() { + return player.getFastForwardIncrement(); + } + + @Override + public void fastForward() { + player.fastForward(); + } + + @Override + public void setRewindIncrement(long rewindIncrementMs) { + player.setRewindIncrement(rewindIncrementMs); + } + + @Override + public long getRewindIncrement() { + return player.getRewindIncrement(); + } + + @Override + public void rewind() { + player.rewind(); + } + @Override public boolean hasPrevious() { return player.hasPrevious(); diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Player.java b/library/common/src/main/java/com/google/android/exoplayer2/Player.java index 8867644629..ba670c4068 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Player.java @@ -22,6 +22,7 @@ import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; import androidx.annotation.IntDef; +import androidx.annotation.IntRange; import androidx.annotation.Nullable; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioListener; @@ -842,6 +843,11 @@ public interface Player { default void onMetadata(Metadata metadata) {} } + /** The default {@link #fastForward()} increment, in milliseconds. */ + long DEFAULT_FAST_FORWARD_INCREMENT_MS = 15_000; + /** The default {@link #rewind()} increment, in milliseconds. */ + long DEFAULT_REWIND_INCREMENT_MS = 5000; + /** * Playback state. One of {@link #STATE_IDLE}, {@link #STATE_BUFFERING}, {@link #STATE_READY} or * {@link #STATE_ENDED}. @@ -1536,6 +1542,46 @@ public interface Player { */ void seekTo(int windowIndex, long positionMs); + /** + * Sets the {@link #fastForward()} increment. + * + * @param fastForwardIncrementMs The fast forward increment, in milliseconds. + * @throws IllegalArgumentException If {@code fastForwardIncrementMs} is non-positive. + */ + void setFastForwardIncrement(@IntRange(from = 1) long fastForwardIncrementMs); + + /** + * Returns the {@link #fastForward()} increment. + * + *

The default value is {@link #DEFAULT_FAST_FORWARD_INCREMENT_MS}. + * + * @return The fast forward increment, in milliseconds. + */ + long getFastForwardIncrement(); + + /** Seeks forward in the current window by {@link #getFastForwardIncrement()} milliseconds. */ + void fastForward(); + + /** + * Sets the {@link #rewind()} increment. + * + * @param rewindIncrementMs The rewind increment, in milliseconds. + * @throws IllegalArgumentException If {@code rewindIncrementMs} is non-positive. + */ + void setRewindIncrement(@IntRange(from = 1) long rewindIncrementMs); + + /** + * Returns the {@link #rewind()} increment. + * + *

The default value is {@link #DEFAULT_REWIND_INCREMENT_MS}. + * + * @return The rewind increment, in milliseconds. + */ + long getRewindIncrement(); + + /** Seeks back in the current window by {@link #getRewindIncrement()} milliseconds. */ + void rewind(); + /** * Returns whether a previous window exists, which may depend on the current repeat mode and * whether shuffle mode is enabled. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index d3b67e9901..0a832aa3b6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import static com.google.android.exoplayer2.util.Assertions.checkArgument; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkState; import static com.google.android.exoplayer2.util.Util.castNonNull; @@ -102,6 +103,8 @@ import java.util.concurrent.CopyOnWriteArraySet; private boolean pauseAtEndOfMediaItems; private Commands availableCommands; private MediaMetadata mediaMetadata; + private long fastForwardIncrementMs; + private long rewindIncrementMs; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; @@ -210,6 +213,8 @@ import java.util.concurrent.CopyOnWriteArraySet; .add(COMMAND_SEEK_TO_MEDIA_ITEM) .build(); mediaMetadata = MediaMetadata.EMPTY; + fastForwardIncrementMs = DEFAULT_FAST_FORWARD_INCREMENT_MS; + rewindIncrementMs = DEFAULT_REWIND_INCREMENT_MS; maskingWindowIndex = C.INDEX_UNSET; playbackInfoUpdateHandler = clock.createHandler(applicationLooper, /* callback= */ null); playbackInfoUpdateListener = @@ -710,6 +715,28 @@ import java.util.concurrent.CopyOnWriteArraySet; oldMaskingWindowIndex); } + @Override + public void setFastForwardIncrement(long fastForwardIncrementMs) { + checkArgument(fastForwardIncrementMs > 0); + this.fastForwardIncrementMs = fastForwardIncrementMs; + } + + @Override + public long getFastForwardIncrement() { + return fastForwardIncrementMs; + } + + @Override + public void setRewindIncrement(long rewindIncrementMs) { + checkArgument(rewindIncrementMs > 0); + this.rewindIncrementMs = rewindIncrementMs; + } + + @Override + public long getRewindIncrement() { + return rewindIncrementMs; + } + @Override public void setPlaybackParameters(PlaybackParameters playbackParameters) { if (playbackParameters == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 6f8df8b996..f5892987d5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -1552,6 +1552,30 @@ public class SimpleExoPlayer extends BasePlayer player.seekTo(windowIndex, positionMs); } + @Override + public void setFastForwardIncrement(long fastForwardIncrementMs) { + verifyApplicationThread(); + player.setFastForwardIncrement(fastForwardIncrementMs); + } + + @Override + public long getFastForwardIncrement() { + verifyApplicationThread(); + return player.getFastForwardIncrement(); + } + + @Override + public void setRewindIncrement(long rewindIncrementMs) { + verifyApplicationThread(); + player.setRewindIncrement(rewindIncrementMs); + } + + @Override + public long getRewindIncrement() { + verifyApplicationThread(); + return player.getRewindIncrement(); + } + @Override public void setPlaybackParameters(PlaybackParameters playbackParameters) { verifyApplicationThread(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 30e2733a4d..3ce78d6cb8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -38,6 +38,8 @@ import static com.google.android.exoplayer2.Player.COMMAND_SET_SHUFFLE_MODE; import static com.google.android.exoplayer2.Player.COMMAND_SET_SPEED_AND_PITCH; import static com.google.android.exoplayer2.Player.COMMAND_SET_VIDEO_SURFACE; import static com.google.android.exoplayer2.Player.COMMAND_SET_VOLUME; +import static com.google.android.exoplayer2.Player.DEFAULT_FAST_FORWARD_INCREMENT_MS; +import static com.google.android.exoplayer2.Player.DEFAULT_REWIND_INCREMENT_MS; import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilPosition; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilStartOfWindow; @@ -10334,6 +10336,128 @@ public final class ExoPlayerTest { player.release(); } + @Test + public void fastForward_callsOnPositionDiscontinuity() throws Exception { + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + Player.Listener listener = mock(Player.Listener.class); + player.addListener(listener); + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, + /* isDynamic= */ true, + /* durationUs= */ C.msToUs(2 * DEFAULT_FAST_FORWARD_INCREMENT_MS))); + player.setMediaSource(new FakeMediaSource(fakeTimeline)); + + player.prepare(); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY); + player.fastForward(); + + ArgumentCaptor oldPosition = + ArgumentCaptor.forClass(Player.PositionInfo.class); + ArgumentCaptor newPosition = + ArgumentCaptor.forClass(Player.PositionInfo.class); + verify(listener, never()) + .onPositionDiscontinuity(any(), any(), not(eq(Player.DISCONTINUITY_REASON_SEEK))); + verify(listener) + .onPositionDiscontinuity( + oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_SEEK)); + List oldPositions = oldPosition.getAllValues(); + List newPositions = newPosition.getAllValues(); + assertThat(oldPositions.get(0).windowIndex).isEqualTo(0); + assertThat(oldPositions.get(0).positionMs).isEqualTo(0); + assertThat(oldPositions.get(0).contentPositionMs).isEqualTo(0); + assertThat(newPositions.get(0).windowIndex).isEqualTo(0); + assertThat(newPositions.get(0).positionMs).isEqualTo(DEFAULT_FAST_FORWARD_INCREMENT_MS); + assertThat(newPositions.get(0).contentPositionMs).isEqualTo(DEFAULT_FAST_FORWARD_INCREMENT_MS); + + player.release(); + } + + @Test + public void fastForward_pastDuration_seeksToDuration() throws Exception { + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, + /* isDynamic= */ true, + /* durationUs= */ C.msToUs(DEFAULT_FAST_FORWARD_INCREMENT_MS / 2))); + player.setMediaSource(new FakeMediaSource(fakeTimeline)); + + player.prepare(); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY); + player.fastForward(); + + assertThat(player.getCurrentPosition()).isEqualTo(DEFAULT_FAST_FORWARD_INCREMENT_MS / 2); + + player.release(); + } + + @Test + public void rewind_callsOnPositionDiscontinuity() throws Exception { + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + Player.Listener listener = mock(Player.Listener.class); + player.addListener(listener); + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, + /* isDynamic= */ true, + /* durationUs= */ C.msToUs(3 * DEFAULT_REWIND_INCREMENT_MS))); + player.setMediaSource(new FakeMediaSource(fakeTimeline)); + + player.prepare(); + TestPlayerRunHelper.playUntilPosition( + player, /* windowIndex= */ 0, /* positionMs= */ 2 * DEFAULT_REWIND_INCREMENT_MS); + player.rewind(); + + ArgumentCaptor oldPosition = + ArgumentCaptor.forClass(Player.PositionInfo.class); + ArgumentCaptor newPosition = + ArgumentCaptor.forClass(Player.PositionInfo.class); + verify(listener, never()) + .onPositionDiscontinuity(any(), any(), not(eq(Player.DISCONTINUITY_REASON_SEEK))); + verify(listener) + .onPositionDiscontinuity( + oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_SEEK)); + List oldPositions = oldPosition.getAllValues(); + List newPositions = newPosition.getAllValues(); + assertThat(oldPositions.get(0).windowIndex).isEqualTo(0); + assertThat(oldPositions.get(0).positionMs) + .isIn(Range.closed(2 * DEFAULT_REWIND_INCREMENT_MS - 20, 2 * DEFAULT_REWIND_INCREMENT_MS)); + assertThat(oldPositions.get(0).contentPositionMs) + .isIn(Range.closed(2 * DEFAULT_REWIND_INCREMENT_MS - 20, 2 * DEFAULT_REWIND_INCREMENT_MS)); + assertThat(newPositions.get(0).windowIndex).isEqualTo(0); + assertThat(newPositions.get(0).positionMs) + .isIn(Range.closed(DEFAULT_REWIND_INCREMENT_MS - 20, DEFAULT_REWIND_INCREMENT_MS)); + assertThat(newPositions.get(0).contentPositionMs) + .isIn(Range.closed(DEFAULT_REWIND_INCREMENT_MS - 20, DEFAULT_REWIND_INCREMENT_MS)); + + player.release(); + } + + @Test + public void rewind_pastZero_seeksToZero() throws Exception { + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* isSeekable= */ true, + /* isDynamic= */ true, + /* durationUs= */ C.msToUs(DEFAULT_REWIND_INCREMENT_MS))); + player.setMediaSource(new FakeMediaSource(fakeTimeline)); + + player.prepare(); + TestPlayerRunHelper.playUntilPosition( + player, /* windowIndex= */ 0, /* positionMs= */ DEFAULT_REWIND_INCREMENT_MS / 2); + player.rewind(); + + assertThat(player.getCurrentPosition()).isEqualTo(0); + + player.release(); + } + @Test public void stop_doesNotCallOnPositionDiscontinuity() throws Exception { ExoPlayer player = new TestExoPlayerBuilder(context).build(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index c3498820dd..82adac4940 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -299,6 +299,26 @@ public class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public void setFastForwardIncrement(long fastForwardIncrementMs) { + throw new UnsupportedOperationException(); + } + + @Override + public long getFastForwardIncrement() { + throw new UnsupportedOperationException(); + } + + @Override + public void setRewindIncrement(long rewindIncrementMs) { + throw new UnsupportedOperationException(); + } + + @Override + public long getRewindIncrement() { + throw new UnsupportedOperationException(); + } + @Override public void setPlaybackParameters(PlaybackParameters playbackParameters) { throw new UnsupportedOperationException();