Forward seek command details to seekTo method in BasePlayer

BasePlayer simplifies implementations by handling all the various
seek methods and forwarding to a single method that can then be
implemented by subclasses. However, this loses the information about
the concrete entry point used for seeking, which is relevant when
the subclass wants to verify or filter by Player.Command. This
can be improved by adding the command as a new parameter. Since
we have to change the method anyway, we can also incorporate the
boolean flag about whether the current item is repeated to avoid
the separate method.

PiperOrigin-RevId: 494948094
(cherry picked from commit ab6fc6a08d0908afe59e7cd17fcaefa96acf1816)
This commit is contained in:
tonihei 2022-12-13 09:04:04 +00:00 committed by christosts
parent 80be30f511
commit cdc07e2175
8 changed files with 485 additions and 108 deletions

View File

@ -13,6 +13,8 @@
playback thread for a new ExoPlayer instance.
* Allow download manager helpers to be cleared
([#10776](https://github.com/google/ExoPlayer/issues/10776)).
* Add parameter to `BasePlayer.seekTo` to also indicate the command used
for seeking.
* Audio:
* Use the compressed audio format bitrate to calculate the min buffer size
for `AudioTrack` in direct playbacks (passthrough).

View File

@ -15,6 +15,7 @@
*/
package androidx.media3.cast;
import static androidx.annotation.VisibleForTesting.PROTECTED;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Util.castNonNull;
import static java.lang.Math.min;
@ -399,7 +400,12 @@ public final class CastPlayer extends BasePlayer {
// don't implement onPositionDiscontinuity().
@SuppressWarnings("deprecation")
@Override
public void seekTo(int mediaItemIndex, long positionMs) {
@VisibleForTesting(otherwise = PROTECTED)
public void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,
boolean isRepeatingCurrentItem) {
MediaStatus mediaStatus = getMediaStatus();
// We assume the default position is 0. There is no support for seeking to the default position
// in RemoteMediaClient.

View File

@ -15,14 +15,15 @@
*/
package androidx.media3.common;
import static androidx.annotation.VisibleForTesting.PROTECTED;
import static java.lang.Math.max;
import static java.lang.Math.min;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.ForOverride;
import java.util.List;
/** Abstract base {@link Player} which implements common implementation independent methods. */
@ -121,27 +122,23 @@ public abstract class BasePlayer implements Player {
@Override
public final void seekToDefaultPosition() {
seekToDefaultPosition(getCurrentMediaItemIndex());
seekToDefaultPositionInternal(
getCurrentMediaItemIndex(), Player.COMMAND_SEEK_TO_DEFAULT_POSITION);
}
@Override
public final void seekToDefaultPosition(int mediaItemIndex) {
seekTo(mediaItemIndex, /* positionMs= */ C.TIME_UNSET);
}
@Override
public final void seekTo(long positionMs) {
seekTo(getCurrentMediaItemIndex(), positionMs);
seekToDefaultPositionInternal(mediaItemIndex, Player.COMMAND_SEEK_TO_MEDIA_ITEM);
}
@Override
public final void seekBack() {
seekToOffset(-getSeekBackIncrement());
seekToOffset(-getSeekBackIncrement(), Player.COMMAND_SEEK_BACK);
}
@Override
public final void seekForward() {
seekToOffset(getSeekForwardIncrement());
seekToOffset(getSeekForwardIncrement(), Player.COMMAND_SEEK_FORWARD);
}
/**
@ -187,15 +184,7 @@ public abstract class BasePlayer implements Player {
@Override
public final void seekToPreviousMediaItem() {
int previousMediaItemIndex = getPreviousMediaItemIndex();
if (previousMediaItemIndex == C.INDEX_UNSET) {
return;
}
if (previousMediaItemIndex == getCurrentMediaItemIndex()) {
repeatCurrentMediaItem();
} else {
seekToDefaultPosition(previousMediaItemIndex);
}
seekToPreviousMediaItemInternal(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
}
@Override
@ -207,12 +196,12 @@ public abstract class BasePlayer implements Player {
boolean hasPreviousMediaItem = hasPreviousMediaItem();
if (isCurrentMediaItemLive() && !isCurrentMediaItemSeekable()) {
if (hasPreviousMediaItem) {
seekToPreviousMediaItem();
seekToPreviousMediaItemInternal(Player.COMMAND_SEEK_TO_PREVIOUS);
}
} else if (hasPreviousMediaItem && getCurrentPosition() <= getMaxSeekToPreviousPosition()) {
seekToPreviousMediaItem();
seekToPreviousMediaItemInternal(Player.COMMAND_SEEK_TO_PREVIOUS);
} else {
seekTo(/* positionMs= */ 0);
seekToCurrentItem(/* positionMs= */ 0, Player.COMMAND_SEEK_TO_PREVIOUS);
}
}
@ -259,15 +248,7 @@ public abstract class BasePlayer implements Player {
@Override
public final void seekToNextMediaItem() {
int nextMediaItemIndex = getNextMediaItemIndex();
if (nextMediaItemIndex == C.INDEX_UNSET) {
return;
}
if (nextMediaItemIndex == getCurrentMediaItemIndex()) {
repeatCurrentMediaItem();
} else {
seekToDefaultPosition(nextMediaItemIndex);
}
seekToNextMediaItemInternal(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
}
@Override
@ -277,12 +258,42 @@ public abstract class BasePlayer implements Player {
return;
}
if (hasNextMediaItem()) {
seekToNextMediaItem();
seekToNextMediaItemInternal(Player.COMMAND_SEEK_TO_NEXT);
} else if (isCurrentMediaItemLive() && isCurrentMediaItemDynamic()) {
seekToDefaultPosition();
seekToDefaultPositionInternal(getCurrentMediaItemIndex(), Player.COMMAND_SEEK_TO_NEXT);
}
}
@Override
public final void seekTo(long positionMs) {
seekToCurrentItem(positionMs, Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM);
}
@Override
public final void seekTo(int mediaItemIndex, long positionMs) {
seekTo(
mediaItemIndex,
positionMs,
Player.COMMAND_SEEK_TO_MEDIA_ITEM,
/* isRepeatingCurrentItem= */ false);
}
/**
* Seeks to a position in the specified {@link MediaItem}.
*
* @param mediaItemIndex The index of the {@link MediaItem}.
* @param positionMs The seek position in the specified {@link MediaItem} in milliseconds, or
* {@link C#TIME_UNSET} to seek to the media item's default position.
* @param seekCommand The {@link Player.Command} used to trigger the seek.
* @param isRepeatingCurrentItem Whether this seeks repeats the current item.
*/
@VisibleForTesting(otherwise = PROTECTED)
public abstract void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,
boolean isRepeatingCurrentItem);
@Override
public final void setPlaybackSpeed(float speed) {
setPlaybackParameters(getPlaybackParameters().withSpeed(speed));
@ -437,29 +448,63 @@ public abstract class BasePlayer implements Player {
: timeline.getWindow(getCurrentMediaItemIndex(), window).getDurationMs();
}
/**
* Repeat the current media item.
*
* <p>The default implementation seeks to the default position in the current item, which can be
* overridden for additional handling.
*/
@ForOverride
protected void repeatCurrentMediaItem() {
seekToDefaultPosition();
}
private @RepeatMode int getRepeatModeForNavigation() {
@RepeatMode int repeatMode = getRepeatMode();
return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode;
}
private void seekToOffset(long offsetMs) {
private void seekToCurrentItem(long positionMs, @Player.Command int seekCommand) {
seekTo(
getCurrentMediaItemIndex(), positionMs, seekCommand, /* isRepeatingCurrentItem= */ false);
}
private void seekToOffset(long offsetMs, @Player.Command int seekCommand) {
long positionMs = getCurrentPosition() + offsetMs;
long durationMs = getDuration();
if (durationMs != C.TIME_UNSET) {
positionMs = min(positionMs, durationMs);
}
positionMs = max(positionMs, 0);
seekTo(positionMs);
seekToCurrentItem(positionMs, seekCommand);
}
private void seekToDefaultPositionInternal(int mediaItemIndex, @Player.Command int seekCommand) {
seekTo(
mediaItemIndex,
/* positionMs= */ C.TIME_UNSET,
seekCommand,
/* isRepeatingCurrentItem= */ false);
}
private void seekToNextMediaItemInternal(@Player.Command int seekCommand) {
int nextMediaItemIndex = getNextMediaItemIndex();
if (nextMediaItemIndex == C.INDEX_UNSET) {
return;
}
if (nextMediaItemIndex == getCurrentMediaItemIndex()) {
repeatCurrentMediaItem(seekCommand);
} else {
seekToDefaultPositionInternal(nextMediaItemIndex, seekCommand);
}
}
private void seekToPreviousMediaItemInternal(@Player.Command int seekCommand) {
int previousMediaItemIndex = getPreviousMediaItemIndex();
if (previousMediaItemIndex == C.INDEX_UNSET) {
return;
}
if (previousMediaItemIndex == getCurrentMediaItemIndex()) {
repeatCurrentMediaItem(seekCommand);
} else {
seekToDefaultPositionInternal(previousMediaItemIndex, seekCommand);
}
}
private void repeatCurrentMediaItem(@Player.Command int seekCommand) {
seekTo(
getCurrentMediaItemIndex(),
/* positionMs= */ C.TIME_UNSET,
seekCommand,
/* isRepeatingCurrentItem= */ true);
}
}

View File

@ -15,6 +15,7 @@
*/
package androidx.media3.common;
import static androidx.annotation.VisibleForTesting.PROTECTED;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
@ -32,6 +33,7 @@ import android.view.TextureView;
import androidx.annotation.FloatRange;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper;
@ -2133,13 +2135,12 @@ public abstract class SimpleBasePlayer extends BasePlayer {
}
@Override
public final void seekTo(int mediaItemIndex, long positionMs) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
protected final void repeatCurrentMediaItem() {
@VisibleForTesting(otherwise = PROTECTED)
public final void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,
boolean isRepeatingCurrentItem) {
// TODO: implement.
throw new IllegalStateException();
}

View File

@ -0,0 +1,318 @@
/*
* Copyright 2022 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.common;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.StubPlayer;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link BasePlayer}. */
@RunWith(AndroidJUnit4.class)
public class BasePlayerTest {
@Test
public void seekTo_withIndexAndPosition_usesCommandSeekToMediaItem() {
BasePlayer player = spy(new TestBasePlayer());
player.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ 4000);
verify(player)
.seekTo(
/* mediaItemIndex= */ 2,
/* positionMs= */ 4000,
Player.COMMAND_SEEK_TO_MEDIA_ITEM,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekTo_withPosition_usesCommandSeekInCurrentMediaItem() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
});
player.seekTo(/* positionMs= */ 4000);
verify(player)
.seekTo(
/* mediaItemIndex= */ 1,
/* positionMs= */ 4000,
Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekToDefaultPosition_withIndex_usesCommandSeekToMediaItem() {
BasePlayer player = spy(new TestBasePlayer());
player.seekToDefaultPosition(/* mediaItemIndex= */ 2);
verify(player)
.seekTo(
/* mediaItemIndex= */ 2,
/* positionMs= */ C.TIME_UNSET,
Player.COMMAND_SEEK_TO_MEDIA_ITEM,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekToDefaultPosition_withoutIndex_usesCommandSeekToDefaultPosition() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
});
player.seekToDefaultPosition();
verify(player)
.seekTo(
/* mediaItemIndex= */ 1,
/* positionMs= */ C.TIME_UNSET,
Player.COMMAND_SEEK_TO_DEFAULT_POSITION,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekToNext_usesCommandSeekToNext() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
});
player.seekToNext();
verify(player)
.seekTo(
/* mediaItemIndex= */ 2,
/* positionMs= */ C.TIME_UNSET,
Player.COMMAND_SEEK_TO_NEXT,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekToNextMediaItem_usesCommandSeekToNextMediaItem() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
});
player.seekToNextMediaItem();
verify(player)
.seekTo(
/* mediaItemIndex= */ 2,
/* positionMs= */ C.TIME_UNSET,
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekForward_usesCommandSeekForward() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public long getSeekForwardIncrement() {
return 2000;
}
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
@Override
public long getCurrentPosition() {
return 5000;
}
});
player.seekForward();
verify(player)
.seekTo(
/* mediaItemIndex= */ 1,
/* positionMs= */ 7000,
Player.COMMAND_SEEK_FORWARD,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekToPrevious_usesCommandSeekToPrevious() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
@Override
public long getMaxSeekToPreviousPosition() {
return 4000;
}
@Override
public long getCurrentPosition() {
return 2000;
}
});
player.seekToPrevious();
verify(player)
.seekTo(
/* mediaItemIndex= */ 0,
/* positionMs= */ C.TIME_UNSET,
Player.COMMAND_SEEK_TO_PREVIOUS,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekToPreviousMediaItem_usesCommandSeekToPreviousMediaItem() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
});
player.seekToPreviousMediaItem();
verify(player)
.seekTo(
/* mediaItemIndex= */ 0,
/* positionMs= */ C.TIME_UNSET,
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekBack_usesCommandSeekBack() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public long getSeekBackIncrement() {
return 2000;
}
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
@Override
public long getCurrentPosition() {
return 5000;
}
});
player.seekBack();
verify(player)
.seekTo(
/* mediaItemIndex= */ 1,
/* positionMs= */ 3000,
Player.COMMAND_SEEK_BACK,
/* isRepeatingCurrentItem= */ false);
}
private static class TestBasePlayer extends StubPlayer {
@Override
public void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,
boolean isRepeatingCurrentItem) {
// Do nothing.
}
@Override
public long getSeekBackIncrement() {
return 2000;
}
@Override
public long getSeekForwardIncrement() {
return 2000;
}
@Override
public long getMaxSeekToPreviousPosition() {
return 2000;
}
@Override
public Timeline getCurrentTimeline() {
return new FakeTimeline(/* windowCount= */ 3);
}
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
@Override
public long getCurrentPosition() {
return 5000;
}
@Override
public long getDuration() {
return 20000;
}
@Override
public boolean isPlayingAd() {
return false;
}
@Override
public int getRepeatMode() {
return Player.REPEAT_MODE_OFF;
}
@Override
public boolean getShuffleModeEnabled() {
return false;
}
}
}

View File

@ -823,16 +823,51 @@ import java.util.concurrent.TimeoutException;
}
@Override
protected void repeatCurrentMediaItem() {
public void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,
boolean isRepeatingCurrentItem) {
verifyApplicationThread();
seekToInternal(
getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET, /* repeatMediaItem= */ true);
analyticsCollector.notifySeekStarted();
Timeline timeline = playbackInfo.timeline;
if (mediaItemIndex < 0
|| (!timeline.isEmpty() && mediaItemIndex >= timeline.getWindowCount())) {
throw new IllegalSeekPositionException(timeline, mediaItemIndex, positionMs);
}
@Override
public void seekTo(int mediaItemIndex, long positionMs) {
verifyApplicationThread();
seekToInternal(mediaItemIndex, positionMs, /* repeatMediaItem= */ false);
pendingOperationAcks++;
if (isPlayingAd()) {
// TODO: Investigate adding support for seeking during ads. This is complicated to do in
// general because the midroll ad preceding the seek destination must be played before the
// content position can be played, if a different ad is playing at the moment.
Log.w(TAG, "seekTo ignored because an ad is playing");
ExoPlayerImplInternal.PlaybackInfoUpdate playbackInfoUpdate =
new ExoPlayerImplInternal.PlaybackInfoUpdate(this.playbackInfo);
playbackInfoUpdate.incrementPendingOperationAcks(1);
playbackInfoUpdateListener.onPlaybackInfoUpdate(playbackInfoUpdate);
return;
}
@Player.State
int newPlaybackState =
getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : STATE_BUFFERING;
int oldMaskingMediaItemIndex = getCurrentMediaItemIndex();
PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackState(newPlaybackState);
newPlaybackInfo =
maskTimelineAndPosition(
newPlaybackInfo,
timeline,
maskWindowPositionMsOrGetPeriodPositionUs(timeline, mediaItemIndex, positionMs));
internalPlayer.seekTo(timeline, mediaItemIndex, Util.msToUs(positionMs));
updatePlaybackInfo(
newPlaybackInfo,
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ true,
/* positionDiscontinuity= */ true,
/* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK,
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
oldMaskingMediaItemIndex,
isRepeatingCurrentItem);
}
@Override
@ -2696,48 +2731,6 @@ import java.util.concurrent.TimeoutException;
}
}
private void seekToInternal(int mediaItemIndex, long positionMs, boolean repeatMediaItem) {
analyticsCollector.notifySeekStarted();
Timeline timeline = playbackInfo.timeline;
if (mediaItemIndex < 0
|| (!timeline.isEmpty() && mediaItemIndex >= timeline.getWindowCount())) {
throw new IllegalSeekPositionException(timeline, mediaItemIndex, positionMs);
}
pendingOperationAcks++;
if (isPlayingAd()) {
// TODO: Investigate adding support for seeking during ads. This is complicated to do in
// general because the midroll ad preceding the seek destination must be played before the
// content position can be played, if a different ad is playing at the moment.
Log.w(TAG, "seekTo ignored because an ad is playing");
ExoPlayerImplInternal.PlaybackInfoUpdate playbackInfoUpdate =
new ExoPlayerImplInternal.PlaybackInfoUpdate(this.playbackInfo);
playbackInfoUpdate.incrementPendingOperationAcks(1);
playbackInfoUpdateListener.onPlaybackInfoUpdate(playbackInfoUpdate);
return;
}
@Player.State
int newPlaybackState =
getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : STATE_BUFFERING;
int oldMaskingMediaItemIndex = getCurrentMediaItemIndex();
PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackState(newPlaybackState);
newPlaybackInfo =
maskTimelineAndPosition(
newPlaybackInfo,
timeline,
maskWindowPositionMsOrGetPeriodPositionUs(timeline, mediaItemIndex, positionMs));
internalPlayer.seekTo(timeline, mediaItemIndex, Util.msToUs(positionMs));
updatePlaybackInfo(
newPlaybackInfo,
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ true,
/* positionDiscontinuity= */ true,
/* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK,
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
oldMaskingMediaItemIndex,
repeatMediaItem);
}
private static DeviceInfo createDeviceInfo(StreamVolumeManager streamVolumeManager) {
return new DeviceInfo(
DeviceInfo.PLAYBACK_TYPE_LOCAL,

View File

@ -15,6 +15,8 @@
*/
package androidx.media3.exoplayer;
import static androidx.annotation.VisibleForTesting.PROTECTED;
import android.content.Context;
import android.media.AudioDeviceInfo;
import android.os.Looper;
@ -1004,10 +1006,16 @@ public class SimpleExoPlayer extends BasePlayer
return player.isLoading();
}
@SuppressWarnings("ForOverride") // Forwarding to ForOverride method in ExoPlayerImpl.
@Override
public void seekTo(int mediaItemIndex, long positionMs) {
@VisibleForTesting(otherwise = PROTECTED)
public void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,
boolean isRepeatingCurrentItem) {
blockUntilConstructorFinished();
player.seekTo(mediaItemIndex, positionMs);
player.seekTo(mediaItemIndex, positionMs, seekCommand, isRepeatingCurrentItem);
}
@Override

View File

@ -147,7 +147,11 @@ public class StubPlayer extends BasePlayer {
}
@Override
public void seekTo(int mediaItemIndex, long positionMs) {
public void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,
boolean isRepeatingCurrentItem) {
throw new UnsupportedOperationException();
}