media/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java
tonihei 11257ecacb Forward presumed no-op seeks to handler methods in (Simple)BasePlayer
Some seek operations are currently filtered in the base classes if
the target index is not explicitly specified and the implicitly
assumed operation doesn't have an obvious target index. (Example:
calling seekToNext() at the last item in a playlist)

This is too opinionated because the actual player implementation
using this base class may be able to handle this seek request (e.g.
by adding new items on the fly or using other logic to select
the most suitable next item).

This can be solved by forwarding all seek requests to the respective
handler methods even if they are a presumed no-op. Also clarify the
Javadoc that the provided index is just an assumption if it wasn't
provided explicitly in the method call.

PiperOrigin-RevId: 624887116
2024-04-15 02:38:29 -07:00

536 lines
15 KiB
Java

/*
* Copyright (C) 2018 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 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 java.util.List;
/** Abstract base {@link Player} which implements common implementation independent methods. */
@UnstableApi
public abstract class BasePlayer implements Player {
protected final Timeline.Window window;
protected BasePlayer() {
window = new Timeline.Window();
}
@Override
public final void setMediaItem(MediaItem mediaItem) {
setMediaItems(ImmutableList.of(mediaItem));
}
@Override
public final void setMediaItem(MediaItem mediaItem, long startPositionMs) {
setMediaItems(ImmutableList.of(mediaItem), /* startIndex= */ 0, startPositionMs);
}
@Override
public final void setMediaItem(MediaItem mediaItem, boolean resetPosition) {
setMediaItems(ImmutableList.of(mediaItem), resetPosition);
}
@Override
public final void setMediaItems(List<MediaItem> mediaItems) {
setMediaItems(mediaItems, /* resetPosition= */ true);
}
@Override
public final void addMediaItem(int index, MediaItem mediaItem) {
addMediaItems(index, ImmutableList.of(mediaItem));
}
@Override
public final void addMediaItem(MediaItem mediaItem) {
addMediaItems(ImmutableList.of(mediaItem));
}
@Override
public final void addMediaItems(List<MediaItem> mediaItems) {
addMediaItems(/* index= */ Integer.MAX_VALUE, mediaItems);
}
@Override
public final void moveMediaItem(int currentIndex, int newIndex) {
if (currentIndex != newIndex) {
moveMediaItems(/* fromIndex= */ currentIndex, /* toIndex= */ currentIndex + 1, newIndex);
}
}
@Override
public final void replaceMediaItem(int index, MediaItem mediaItem) {
replaceMediaItems(
/* fromIndex= */ index, /* toIndex= */ index + 1, ImmutableList.of(mediaItem));
}
@Override
public final void removeMediaItem(int index) {
removeMediaItems(/* fromIndex= */ index, /* toIndex= */ index + 1);
}
@Override
public final void clearMediaItems() {
removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ Integer.MAX_VALUE);
}
@Override
public final boolean isCommandAvailable(@Command int command) {
return getAvailableCommands().contains(command);
}
/**
* {@inheritDoc}
*
* <p>BasePlayer and its descendants will return {@code true}.
*/
@Override
public final boolean canAdvertiseSession() {
return true;
}
@Override
public final void play() {
setPlayWhenReady(true);
}
@Override
public final void pause() {
setPlayWhenReady(false);
}
@Override
public final boolean isPlaying() {
return getPlaybackState() == Player.STATE_READY
&& getPlayWhenReady()
&& getPlaybackSuppressionReason() == PLAYBACK_SUPPRESSION_REASON_NONE;
}
@Override
public final void seekToDefaultPosition() {
seekToDefaultPositionInternal(
getCurrentMediaItemIndex(), Player.COMMAND_SEEK_TO_DEFAULT_POSITION);
}
@Override
public final void seekToDefaultPosition(int mediaItemIndex) {
seekToDefaultPositionInternal(mediaItemIndex, Player.COMMAND_SEEK_TO_MEDIA_ITEM);
}
@Override
public final void seekBack() {
seekToOffset(-getSeekBackIncrement(), Player.COMMAND_SEEK_BACK);
}
@Override
public final void seekForward() {
seekToOffset(getSeekForwardIncrement(), Player.COMMAND_SEEK_FORWARD);
}
/**
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@Deprecated
@Override
public final boolean hasPrevious() {
return hasPreviousMediaItem();
}
/**
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@Deprecated
@Override
public final boolean hasPreviousWindow() {
return hasPreviousMediaItem();
}
@Override
public final boolean hasPreviousMediaItem() {
return getPreviousMediaItemIndex() != C.INDEX_UNSET;
}
/**
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@Deprecated
@Override
public final void previous() {
seekToPreviousMediaItem();
}
/**
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@Deprecated
@Override
public final void seekToPreviousWindow() {
seekToPreviousMediaItem();
}
@Override
public final void seekToPreviousMediaItem() {
seekToPreviousMediaItemInternal(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
}
@Override
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);
} else {
seekToCurrentItem(/* positionMs= */ 0, Player.COMMAND_SEEK_TO_PREVIOUS);
}
}
/**
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@Deprecated
@Override
public final boolean hasNext() {
return hasNextMediaItem();
}
/**
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@Deprecated
@Override
public final boolean hasNextWindow() {
return hasNextMediaItem();
}
@Override
public final boolean hasNextMediaItem() {
return getNextMediaItemIndex() != C.INDEX_UNSET;
}
/**
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@Deprecated
@Override
public final void next() {
seekToNextMediaItem();
}
/**
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@Deprecated
@Override
public final void seekToNextWindow() {
seekToNextMediaItem();
}
@Override
public final void seekToNextMediaItem() {
seekToNextMediaItemInternal(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
}
@Override
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);
}
}
@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}. 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. 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.
*/
@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));
}
/**
* @deprecated Use {@link #getCurrentMediaItemIndex()} instead.
*/
@Deprecated
@Override
public final int getCurrentWindowIndex() {
return getCurrentMediaItemIndex();
}
/**
* @deprecated Use {@link #getNextMediaItemIndex()} instead.
*/
@Deprecated
@Override
public final int getNextWindowIndex() {
return getNextMediaItemIndex();
}
@Override
public final int getNextMediaItemIndex() {
Timeline timeline = getCurrentTimeline();
return timeline.isEmpty()
? C.INDEX_UNSET
: timeline.getNextWindowIndex(
getCurrentMediaItemIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled());
}
/**
* @deprecated Use {@link #getPreviousMediaItemIndex()} instead.
*/
@Deprecated
@Override
public final int getPreviousWindowIndex() {
return getPreviousMediaItemIndex();
}
@Override
public final int getPreviousMediaItemIndex() {
Timeline timeline = getCurrentTimeline();
return timeline.isEmpty()
? C.INDEX_UNSET
: timeline.getPreviousWindowIndex(
getCurrentMediaItemIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled());
}
@Override
@Nullable
public final MediaItem getCurrentMediaItem() {
Timeline timeline = getCurrentTimeline();
return timeline.isEmpty()
? null
: timeline.getWindow(getCurrentMediaItemIndex(), window).mediaItem;
}
@Override
public final int getMediaItemCount() {
return getCurrentTimeline().getWindowCount();
}
@Override
public final MediaItem getMediaItemAt(int index) {
return getCurrentTimeline().getWindow(index, window).mediaItem;
}
@Override
@Nullable
public final Object getCurrentManifest() {
Timeline timeline = getCurrentTimeline();
return timeline.isEmpty()
? null
: timeline.getWindow(getCurrentMediaItemIndex(), window).manifest;
}
@Override
public final int getBufferedPercentage() {
long position = getBufferedPosition();
long duration = getDuration();
return position == C.TIME_UNSET || duration == C.TIME_UNSET
? 0
: duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
}
/**
* @deprecated Use {@link #isCurrentMediaItemDynamic()} instead.
*/
@Deprecated
@Override
public final boolean isCurrentWindowDynamic() {
return isCurrentMediaItemDynamic();
}
@Override
public final boolean isCurrentMediaItemDynamic() {
Timeline timeline = getCurrentTimeline();
return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isDynamic;
}
/**
* @deprecated Use {@link #isCurrentMediaItemLive()} instead.
*/
@Deprecated
@Override
public final boolean isCurrentWindowLive() {
return isCurrentMediaItemLive();
}
@Override
public final boolean isCurrentMediaItemLive() {
Timeline timeline = getCurrentTimeline();
return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isLive();
}
@Override
public final long getCurrentLiveOffset() {
Timeline timeline = getCurrentTimeline();
if (timeline.isEmpty()) {
return C.TIME_UNSET;
}
long windowStartTimeMs =
timeline.getWindow(getCurrentMediaItemIndex(), window).windowStartTimeMs;
if (windowStartTimeMs == C.TIME_UNSET) {
return C.TIME_UNSET;
}
return window.getCurrentUnixTimeMs() - window.windowStartTimeMs - getContentPosition();
}
/**
* @deprecated Use {@link #isCurrentMediaItemSeekable()} instead.
*/
@Deprecated
@Override
public final boolean isCurrentWindowSeekable() {
return isCurrentMediaItemSeekable();
}
@Override
public final boolean isCurrentMediaItemSeekable() {
Timeline timeline = getCurrentTimeline();
return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isSeekable;
}
@Override
public final long getContentDuration() {
Timeline timeline = getCurrentTimeline();
return timeline.isEmpty()
? C.TIME_UNSET
: timeline.getWindow(getCurrentMediaItemIndex(), window).getDurationMs();
}
private @RepeatMode int getRepeatModeForNavigation() {
@RepeatMode int repeatMode = getRepeatMode();
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);
}
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);
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) {
ignoreSeek(seekCommand);
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) {
ignoreSeek(seekCommand);
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);
}
}