diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 90695c3c0e..2e898d948b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,6 +3,10 @@ ### Unreleased changes * Common Library: + * Forward presumed no-op seek calls to the protected `BasePlayer.seekTo` + and `SimpleBasePlayer.handleSeek` methods instead of ignoring them. If + you are implementing these methods in a custom player, you may need to + handle these additional calls with `mediaItemIndex == C.INDEX_UNSET`. * ExoPlayer: * Add `reset` to `BasePreloadManager` to release all the holding sources while keep the preload manager instance. diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java index 884169151e..f18fde331c 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java @@ -417,6 +417,9 @@ public final class CastPlayer extends BasePlayer { long positionMs, @Player.Command int seekCommand, boolean isRepeatingCurrentItem) { + if (mediaItemIndex == C.INDEX_UNSET) { + return; + } checkArgument(mediaItemIndex >= 0); if (!currentTimeline.isEmpty() && mediaItemIndex >= currentTimeline.getWindowCount()) { return; diff --git a/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java index 7910d474a8..31036bf9fb 100644 --- a/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java @@ -197,12 +197,15 @@ public abstract class BasePlayer implements Player { public final void seekToPrevious() { Timeline timeline = getCurrentTimeline(); if (timeline.isEmpty() || isPlayingAd()) { + ignoreSeek(Player.COMMAND_SEEK_TO_PREVIOUS); return; } boolean hasPreviousMediaItem = hasPreviousMediaItem(); if (isCurrentMediaItemLive() && !isCurrentMediaItemSeekable()) { if (hasPreviousMediaItem) { seekToPreviousMediaItemInternal(Player.COMMAND_SEEK_TO_PREVIOUS); + } else { + ignoreSeek(Player.COMMAND_SEEK_TO_PREVIOUS); } } else if (hasPreviousMediaItem && getCurrentPosition() <= getMaxSeekToPreviousPosition()) { seekToPreviousMediaItemInternal(Player.COMMAND_SEEK_TO_PREVIOUS); @@ -261,12 +264,15 @@ public abstract class BasePlayer implements Player { public final void seekToNext() { Timeline timeline = getCurrentTimeline(); if (timeline.isEmpty() || isPlayingAd()) { + ignoreSeek(Player.COMMAND_SEEK_TO_NEXT); return; } if (hasNextMediaItem()) { seekToNextMediaItemInternal(Player.COMMAND_SEEK_TO_NEXT); } else if (isCurrentMediaItemLive() && isCurrentMediaItemDynamic()) { seekToDefaultPositionInternal(getCurrentMediaItemIndex(), Player.COMMAND_SEEK_TO_NEXT); + } else { + ignoreSeek(Player.COMMAND_SEEK_TO_NEXT); } } @@ -287,9 +293,13 @@ public abstract class BasePlayer implements Player { /** * Seeks to a position in the specified {@link MediaItem}. * - * @param mediaItemIndex The index of the {@link MediaItem}. + * @param mediaItemIndex The index of the {@link MediaItem}. If the original seek operation did + * not directly specify an index, this is the most likely implied index based on the available + * player state. If the implied action is to do nothing, this will be {@link C#INDEX_UNSET}. * @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. + * {@link C#TIME_UNSET} to seek to the media item's default position. If the original seek + * operation did not directly specify a position, this is the most likely implied position + * based on the available player state. * @param seekCommand The {@link Player.Command} used to trigger the seek. * @param isRepeatingCurrentItem Whether this seeks repeats the current item. */ @@ -459,6 +469,14 @@ public abstract class BasePlayer implements Player { return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode; } + private void ignoreSeek(@Player.Command int seekCommand) { + seekTo( + /* mediaItemIndex= */ C.INDEX_UNSET, + /* positionMs= */ C.TIME_UNSET, + seekCommand, + /* isRepeatingCurrentItem= */ false); + } + private void seekToCurrentItem(long positionMs, @Player.Command int seekCommand) { seekTo( getCurrentMediaItemIndex(), positionMs, seekCommand, /* isRepeatingCurrentItem= */ false); @@ -485,6 +503,7 @@ public abstract class BasePlayer implements Player { private void seekToNextMediaItemInternal(@Player.Command int seekCommand) { int nextMediaItemIndex = getNextMediaItemIndex(); if (nextMediaItemIndex == C.INDEX_UNSET) { + ignoreSeek(seekCommand); return; } if (nextMediaItemIndex == getCurrentMediaItemIndex()) { @@ -497,6 +516,7 @@ public abstract class BasePlayer implements Player { private void seekToPreviousMediaItemInternal(@Player.Command int seekCommand) { int previousMediaItemIndex = getPreviousMediaItemIndex(); if (previousMediaItemIndex == C.INDEX_UNSET) { + ignoreSeek(seekCommand); return; } if (previousMediaItemIndex == getCurrentMediaItemIndex()) { diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index a46ff5ddba..0c6b6e12b3 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -2352,19 +2352,24 @@ public abstract class SimpleBasePlayer extends BasePlayer { @Player.Command int seekCommand, boolean isRepeatingCurrentItem) { verifyApplicationThreadAndInitState(); - checkArgument(mediaItemIndex >= 0); + checkArgument(mediaItemIndex == C.INDEX_UNSET || mediaItemIndex >= 0); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!shouldHandleCommand(seekCommand) - || isPlayingAd() - || (!state.playlist.isEmpty() && mediaItemIndex >= state.playlist.size())) { + if (!shouldHandleCommand(seekCommand)) { return; } + boolean ignoreSeekForPlaceholderState = + mediaItemIndex == C.INDEX_UNSET + || isPlayingAd() + || (!state.playlist.isEmpty() && mediaItemIndex >= state.playlist.size()); updateStateForPendingOperation( /* pendingOperation= */ handleSeek(mediaItemIndex, positionMs, seekCommand), /* placeholderStateSupplier= */ () -> - getStateWithNewPlaylistAndPosition(state, state.playlist, mediaItemIndex, positionMs), - /* seeked= */ true, + ignoreSeekForPlaceholderState + ? state + : getStateWithNewPlaylistAndPosition( + state, state.playlist, mediaItemIndex, positionMs), + /* forceSeekDiscontinuity= */ !ignoreSeekForPlaceholderState, isRepeatingCurrentItem); } @@ -2921,7 +2926,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { return; } updateStateAndInformListeners( - getState(), /* seeked= */ false, /* isRepeatingCurrentItem= */ false); + getState(), /* forceSeekDiscontinuity= */ false, /* isRepeatingCurrentItem= */ false); } /** @@ -3344,10 +3349,13 @@ public abstract class SimpleBasePlayer extends BasePlayer { *
Will only be called if the appropriate {@link Player.Command}, for example {@link
* Player#COMMAND_SEEK_TO_MEDIA_ITEM} or {@link Player#COMMAND_SEEK_TO_NEXT}, is available.
*
- * @param mediaItemIndex The media item index to seek to. The index is in the range 0 <= {@code
- * mediaItemIndex} < {@code mediaItems.size()}.
+ * @param mediaItemIndex The media item index to seek to. If the original seek operation did not
+ * directly specify an index, this is the most likely implied index based on the available
+ * player state. If the implied action is to do nothing, this will be {@link C#INDEX_UNSET}.
* @param positionMs The position in milliseconds to start playback from, or {@link C#TIME_UNSET}
- * to start at the default position in the media item.
+ * to start at the default position in the media item. If the original seek operation did not
+ * directly specify a position, this is the most likely implied position based on the
+ * available player state.
* @param seekCommand The {@link Player.Command} used to trigger the seek.
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
* changes caused by this call.
@@ -3385,7 +3393,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
@SuppressWarnings("deprecation") // Calling deprecated listener methods.
@RequiresNonNull("state")
private void updateStateAndInformListeners(
- State newState, boolean seeked, boolean isRepeatingCurrentItem) {
+ State newState, boolean forceSeekDiscontinuity, boolean isRepeatingCurrentItem) {
State previousState = state;
// Assign new state immediately such that all getters return the right values, but use a
// snapshot of the previous and new state so that listener invocations are triggered correctly.
@@ -3407,7 +3415,8 @@ public abstract class SimpleBasePlayer extends BasePlayer {
MediaMetadata previousMediaMetadata = getMediaMetadataInternal(previousState);
MediaMetadata newMediaMetadata = getMediaMetadataInternal(newState);
int positionDiscontinuityReason =
- getPositionDiscontinuityReason(previousState, newState, seeked, window, period);
+ getPositionDiscontinuityReason(
+ previousState, newState, forceSeekDiscontinuity, window, period);
boolean timelineChanged = !previousState.timeline.equals(newState.timeline);
int mediaItemTransitionReason =
getMediaItemTransitionReason(
@@ -3618,7 +3627,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
updateStateForPendingOperation(
pendingOperation,
placeholderStateSupplier,
- /* seeked= */ false,
+ /* forceSeekDiscontinuity= */ false,
/* isRepeatingCurrentItem= */ false);
}
@@ -3626,22 +3635,26 @@ public abstract class SimpleBasePlayer extends BasePlayer {
private void updateStateForPendingOperation(
ListenableFuture> pendingOperation,
Supplier