BasePlayer.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 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()) {
      return;
    }
    boolean hasPreviousMediaItem = hasPreviousMediaItem();
    if (isCurrentMediaItemLive() && !isCurrentMediaItemSeekable()) {
      if (hasPreviousMediaItem) {
        seekToPreviousMediaItemInternal(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()) {
      return;
    }
    if (hasNextMediaItem()) {
      seekToNextMediaItemInternal(Player.COMMAND_SEEK_TO_NEXT);
    } else if (isCurrentMediaItemLive() && isCurrentMediaItemDynamic()) {
      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));
  }

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