Add fast forward and rewind methods to Player

PiperOrigin-RevId: 378104210
This commit is contained in:
kimvde 2021-06-08 10:01:41 +01:00 committed by bachinger
parent d0dc72fb6a
commit e4263c4a67
10 changed files with 422 additions and 6 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> 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<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> 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<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> 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<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> 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();

View file

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