mirror of
https://github.com/samsonjs/media.git
synced 2026-03-26 09:35:47 +00:00
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
536 lines
15 KiB
Java
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);
|
|
}
|
|
}
|