Gradle dependencies
compile group: 'androidx.media3', name: 'media3-cast', version: '1.5.0-alpha01'
- groupId: androidx.media3
 - artifactId: media3-cast
 - version: 1.5.0-alpha01
 
Artifact androidx.media3:media3-cast:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)
Overview
Player implementation that communicates with a Cast receiver app.
 
The behavior of this class depends on the underlying Cast session, which is obtained from the
 injected . To keep track of the session, CastPlayer.isCastSessionAvailable() can
 be queried and SessionAvailabilityListener can be implemented and attached to the player.
 
If no session is available, the player state will remain unchanged and calls to methods that
 alter it will be ignored. Querying the player state is possible even when no session is
 available, in which case, the last observed receiver app state is reported.
 
Methods should be called on the application's main thread.
Summary
| Constructors | 
|---|
| public | CastPlayer(CastContext castContext)
 Creates a new cast player.  | 
| public | CastPlayer(CastContext castContext, MediaItemConverter mediaItemConverter)
 Creates a new cast player.  | 
| public | CastPlayer(CastContext castContext, MediaItemConverter mediaItemConverter, long seekBackIncrementMs, long seekForwardIncrementMs)
 Creates a new cast player.  | 
| public | CastPlayer(Context context, CastContext castContext, MediaItemConverter mediaItemConverter, long seekBackIncrementMs, long seekForwardIncrementMs, long maxSeekToPreviousPositionMs)
 Creates a new cast player.  | 
| Methods | 
|---|
| public void | addListener(Player.Listener listener)
  | 
| public void | addMediaItems(int index, java.util.List<MediaItem> mediaItems)
  | 
| public void | clearVideoSurface()
 This method is not supported and does nothing.  | 
| public void | clearVideoSurface(Surface surface)
 This method is not supported and does nothing.  | 
| public void | clearVideoSurfaceHolder(SurfaceHolder surfaceHolder)
 This method is not supported and does nothing.  | 
| public void | clearVideoSurfaceView(SurfaceView surfaceView)
 This method is not supported and does nothing.  | 
| public void | clearVideoTextureView(TextureView textureView)
 This method is not supported and does nothing.  | 
| public void | decreaseDeviceVolume()
  | 
| public void | decreaseDeviceVolume(int flags)
 This method is not supported and does nothing.  | 
| public Looper | getApplicationLooper()
  | 
| public AudioAttributes | getAudioAttributes()
 This method is not supported and returns AudioAttributes.DEFAULT.  | 
| public Player.Commands | getAvailableCommands()
  | 
| public long | getBufferedPosition()
  | 
| public long | getContentBufferedPosition()
  | 
| public long | getContentPosition()
  | 
| public int | getCurrentAdGroupIndex()
  | 
| public int | getCurrentAdIndexInAdGroup()
  | 
| public CueGroup | getCurrentCues()
 This method is not supported and returns an empty CueGroup.  | 
| public int | getCurrentMediaItemIndex()
  | 
| public int | getCurrentPeriodIndex()
  | 
| public long | getCurrentPosition()
  | 
| public Timeline | getCurrentTimeline()
  | 
| public Tracks | getCurrentTracks()
  | 
| public DeviceInfo | getDeviceInfo()
 Returns a DeviceInfo describing the receiver device.  | 
| public int | getDeviceVolume()
 This method is not supported and always returns 0.  | 
| public long | getDuration()
  | 
| public MediaQueueItem | getItem(int periodId)
 Returns the item that corresponds to the period with the given id, or null if no media queue or
 period with id periodId exist.  | 
| public long | getMaxSeekToPreviousPosition()
  | 
| public MediaMetadata | getMediaMetadata()
  | 
| public MediaMetadata | getMediaMetadataInternal()
  | 
| public PlaybackParameters | getPlaybackParameters()
  | 
| public int | getPlaybackState()
  | 
| public int | getPlaybackSuppressionReason()
  | 
| public PlaybackException | getPlayerError()
  | 
| public MediaMetadata | getPlaylistMetadata()
  | 
| public boolean | getPlayWhenReady()
  | 
| public int | getRepeatMode()
  | 
| public long | getSeekBackIncrement()
  | 
| public long | getSeekForwardIncrement()
  | 
| public boolean | getShuffleModeEnabled()
  | 
| public Size | getSurfaceSize()
 This method is not supported and returns Size.UNKNOWN.  | 
| public long | getTotalBufferedDuration()
  | 
| public TrackSelectionParameters | getTrackSelectionParameters()
  | 
| public VideoSize | getVideoSize()
 This method is not supported and returns VideoSize.UNKNOWN.  | 
| public float | getVolume()
 This method is not supported and returns 1.  | 
| public void | increaseDeviceVolume()
  | 
| public void | increaseDeviceVolume(int flags)
 This method is not supported and does nothing.  | 
| public boolean | isCastSessionAvailable()
 Returns whether a cast session is available.  | 
| public boolean | isDeviceMuted()
 This method is not supported and always returns false.  | 
| public boolean | isLoading()
  | 
| public boolean | isPlayingAd()
  | 
| public void | moveMediaItems(int fromIndex, int toIndex, int newIndex)
  | 
| public void | prepare()
  | 
| public void | release()
  | 
| public void | removeListener(Player.Listener listener)
  | 
| public void | removeMediaItems(int fromIndex, int toIndex)
  | 
| public void | replaceMediaItems(int fromIndex, int toIndex, java.util.List<MediaItem> mediaItems)
  | 
| public abstract void | seekTo(int mediaItemIndex, long positionMs, int seekCommand, boolean isRepeatingCurrentItem)
 Seeks to a position in the specified MediaItem.  | 
| public void | setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus)
 This method is not supported and does nothing.  | 
| public void | setDeviceMuted(boolean muted)
  | 
| public void | setDeviceMuted(boolean muted, int flags)
 This method is not supported and does nothing.  | 
| public void | setDeviceVolume(int volume)
  | 
| public void | setDeviceVolume(int volume, int flags)
 This method is not supported and does nothing.  | 
| public void | setMediaItems(java.util.List<MediaItem> mediaItems, boolean resetPosition)
  | 
| public void | setMediaItems(java.util.List<MediaItem> mediaItems, int startIndex, long startPositionMs)
  | 
| public void | setPlaybackParameters(PlaybackParameters playbackParameters)
  | 
| public void | setPlaylistMetadata(MediaMetadata mediaMetadata)
 This method is not supported and does nothing.  | 
| public void | setPlayWhenReady(boolean playWhenReady)
  | 
| public void | setRepeatMode(int repeatMode)
  | 
| public void | setSessionAvailabilityListener(SessionAvailabilityListener listener)
 Sets a listener for updates on the cast session availability.  | 
| public void | setShuffleModeEnabled(boolean shuffleModeEnabled)
  | 
| public void | setTrackSelectionParameters(TrackSelectionParameters parameters)
  | 
| public void | setVideoSurface(Surface surface)
 This method is not supported and does nothing.  | 
| public void | setVideoSurfaceHolder(SurfaceHolder surfaceHolder)
 This method is not supported and does nothing.  | 
| public void | setVideoSurfaceView(SurfaceView surfaceView)
 This method is not supported and does nothing.  | 
| public void | setVideoTextureView(TextureView textureView)
 This method is not supported and does nothing.  | 
| public void | setVolume(float volume)
 This method is not supported and does nothing.  | 
| public void | stop()
  | 
| from BasePlayer | addMediaItem, addMediaItem, addMediaItems, canAdvertiseSession, clearMediaItems, getBufferedPercentage, getContentDuration, getCurrentLiveOffset, getCurrentManifest, getCurrentMediaItem, getCurrentWindowIndex, getMediaItemAt, getMediaItemCount, getNextMediaItemIndex, getNextWindowIndex, getPreviousMediaItemIndex, getPreviousWindowIndex, hasNext, hasNextMediaItem, hasNextWindow, hasPreviousMediaItem, isCommandAvailable, isCurrentMediaItemDynamic, isCurrentMediaItemLive, isCurrentMediaItemSeekable, isCurrentWindowDynamic, isCurrentWindowLive, isCurrentWindowSeekable, isPlaying, moveMediaItem, next, pause, play, removeMediaItem, replaceMediaItem, seekBack, seekForward, seekTo, seekTo, seekToDefaultPosition, seekToDefaultPosition, seekToNext, seekToNextMediaItem, seekToNextWindow, seekToPrevious, seekToPreviousMediaItem, seekToPreviousWindow, setMediaItem, setMediaItem, setMediaItem, setMediaItems, setPlaybackSpeed | 
| from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait | 
Fields
public static final 
DeviceInfo DEVICE_INFO_REMOTE_EMPTYA remote DeviceInfo with a null DeviceInfo.routingControllerId.
public static final float 
MIN_SPEED_SUPPORTEDpublic static final float 
MAX_SPEED_SUPPORTEDConstructors
public 
CastPlayer(CastContext castContext)
Creates a new cast player.
 
The returned player uses a DefaultMediaItemConverter and
 
mediaItemConverter is set to a DefaultMediaItemConverter, seekBackIncrementMs is set to C.DEFAULT_SEEK_BACK_INCREMENT_MS and seekForwardIncrementMs is set to C.DEFAULT_SEEK_FORWARD_INCREMENT_MS.
Parameters:
castContext: The context from which the cast session is obtained.
Creates a new cast player.
 
seekBackIncrementMs is set to C.DEFAULT_SEEK_BACK_INCREMENT_MS and seekForwardIncrementMs is set to C.DEFAULT_SEEK_FORWARD_INCREMENT_MS.
Parameters:
castContext: The context from which the cast session is obtained.
mediaItemConverter: The MediaItemConverter to use.
public 
CastPlayer(CastContext castContext, 
MediaItemConverter mediaItemConverter, long seekBackIncrementMs, long seekForwardIncrementMs)
Creates a new cast player.
Parameters:
castContext: The context from which the cast session is obtained.
mediaItemConverter: The MediaItemConverter to use.
seekBackIncrementMs: The BasePlayer.seekBack() increment, in milliseconds.
seekForwardIncrementMs: The BasePlayer.seekForward() increment, in milliseconds.
public 
CastPlayer(Context context, CastContext castContext, 
MediaItemConverter mediaItemConverter, long seekBackIncrementMs, long seekForwardIncrementMs, long maxSeekToPreviousPositionMs)
Creates a new cast player.
Parameters:
context: A  used to populate CastPlayer.getDeviceInfo(). If null, CastPlayer.getDeviceInfo() will always return CastPlayer.DEVICE_INFO_REMOTE_EMPTY.
castContext: The context from which the cast session is obtained.
mediaItemConverter: The MediaItemConverter to use.
seekBackIncrementMs: The BasePlayer.seekBack() increment, in milliseconds.
seekForwardIncrementMs: The BasePlayer.seekForward() increment, in milliseconds.
maxSeekToPreviousPositionMs: The maximum position for which BasePlayer.seekToPrevious()
     seeks to the previous MediaItem, in milliseconds.
Methods
public MediaQueueItem 
getItem(int periodId)
Returns the item that corresponds to the period with the given id, or null if no media queue or
 period with id periodId exist.
Parameters:
periodId: The id of the period (CastPlayer.getCurrentTimeline()) that corresponds to the item
     to get.
Returns:
The item that corresponds to the period with the given id, or null if no media queue or
     period with id periodId exist.
public boolean 
isCastSessionAvailable()
Returns whether a cast session is available.
Sets a listener for updates on the cast session availability.
Parameters:
listener: The SessionAvailabilityListener, or null to clear the listener.
public Looper 
getApplicationLooper()
public void 
setMediaItems(java.util.List<MediaItem> mediaItems, boolean resetPosition)
public void 
setMediaItems(java.util.List<MediaItem> mediaItems, int startIndex, long startPositionMs)
public void 
addMediaItems(int index, java.util.List<MediaItem> mediaItems)
public void 
moveMediaItems(int fromIndex, int toIndex, int newIndex)
public void 
replaceMediaItems(int fromIndex, int toIndex, java.util.List<MediaItem> mediaItems)
public void 
removeMediaItems(int fromIndex, int toIndex)
public int 
getPlaybackState()
public int 
getPlaybackSuppressionReason()
public void 
setPlayWhenReady(boolean playWhenReady)
public boolean 
getPlayWhenReady()
public abstract void 
seekTo(int mediaItemIndex, long positionMs, int seekCommand, boolean isRepeatingCurrentItem)
Seeks to a position in the specified MediaItem.
Parameters:
mediaItemIndex: The index of the 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 C.INDEX_UNSET.
positionMs: The seek position in the specified MediaItem in milliseconds, or
     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.
seekCommand: The Player.Command used to trigger the seek.
isRepeatingCurrentItem: Whether this seeks repeats the current item.
public long 
getSeekBackIncrement()
public long 
getSeekForwardIncrement()
public long 
getMaxSeekToPreviousPosition()
public void 
setRepeatMode(int repeatMode)
public int 
getRepeatMode()
public void 
setShuffleModeEnabled(boolean shuffleModeEnabled)
public boolean 
getShuffleModeEnabled()
public 
Tracks getCurrentTracks()
This method is not supported and does nothing.
public int 
getCurrentPeriodIndex()
public int 
getCurrentMediaItemIndex()
public long 
getDuration()
public long 
getCurrentPosition()
public long 
getBufferedPosition()
public long 
getTotalBufferedDuration()
public boolean 
isPlayingAd()
public int 
getCurrentAdGroupIndex()
public int 
getCurrentAdIndexInAdGroup()
public boolean 
isLoading()
public long 
getContentPosition()
public long 
getContentBufferedPosition()
This method is not supported and returns AudioAttributes.DEFAULT.
public void 
setVolume(float volume)
This method is not supported and does nothing.
This method is not supported and returns 1.
public void 
clearVideoSurface()
This method is not supported and does nothing.
public void 
clearVideoSurface(Surface surface)
This method is not supported and does nothing.
public void 
setVideoSurface(Surface surface)
This method is not supported and does nothing.
public void 
setVideoSurfaceHolder(SurfaceHolder surfaceHolder)
This method is not supported and does nothing.
public void 
clearVideoSurfaceHolder(SurfaceHolder surfaceHolder)
This method is not supported and does nothing.
public void 
setVideoSurfaceView(SurfaceView surfaceView)
This method is not supported and does nothing.
public void 
clearVideoSurfaceView(SurfaceView surfaceView)
This method is not supported and does nothing.
public void 
setVideoTextureView(TextureView textureView)
This method is not supported and does nothing.
public void 
clearVideoTextureView(TextureView textureView)
This method is not supported and does nothing.
This method is not supported and returns VideoSize.UNKNOWN.
public 
Size getSurfaceSize()
This method is not supported and returns Size.UNKNOWN.
This method is not supported and returns an empty CueGroup.
Returns a DeviceInfo describing the receiver device. Returns CastPlayer.DEVICE_INFO_REMOTE_EMPTY if no  was provided at construction, or if the Cast
  could not be identified.
public int 
getDeviceVolume()
This method is not supported and always returns 0.
public boolean 
isDeviceMuted()
This method is not supported and always returns false.
public void 
setDeviceVolume(int volume)
Deprecated: Use CastPlayer.setDeviceVolume(int, int) instead.
public void 
setDeviceVolume(int volume, int flags)
This method is not supported and does nothing.
public void 
increaseDeviceVolume()
Deprecated: Use CastPlayer.increaseDeviceVolume(int) instead.
public void 
increaseDeviceVolume(int flags)
This method is not supported and does nothing.
public void 
decreaseDeviceVolume()
Deprecated: Use CastPlayer.decreaseDeviceVolume(int) instead.
public void 
decreaseDeviceVolume(int flags)
This method is not supported and does nothing.
public void 
setDeviceMuted(boolean muted)
Deprecated: Use CastPlayer.setDeviceMuted(boolean, int) instead.
public void 
setDeviceMuted(boolean muted, int flags)
This method is not supported and does nothing.
public void 
setAudioAttributes(
AudioAttributes audioAttributes, boolean handleAudioFocus)
This method is not supported and does nothing.
Source
/*
 * Copyright (C) 2017 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.cast;
import static androidx.annotation.VisibleForTesting.PROTECTED;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Util.SDK_INT;
import static androidx.media3.common.util.Util.castNonNull;
import static java.lang.Math.min;
import android.content.Context;
import android.media.MediaRouter2;
import android.media.MediaRouter2.RouteCallback;
import android.media.MediaRouter2.RoutingController;
import android.media.MediaRouter2.TransferCallback;
import android.media.RouteDiscoveryPreference;
import android.os.Handler;
import android.os.Looper;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import androidx.annotation.DoNotInline;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.BasePlayer;
import androidx.media3.common.C;
import androidx.media3.common.DeviceInfo;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.android.gms.cast.CastStatusCodes;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.MediaTrack;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.cast.framework.CastSession;
import com.google.android.gms.cast.framework.SessionManager;
import com.google.android.gms.cast.framework.SessionManagerListener;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.common.collect.ImmutableList;
import java.util.List;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
 * {@link Player} implementation that communicates with a Cast receiver app.
 *
 * <p>The behavior of this class depends on the underlying Cast session, which is obtained from the
 * injected {@link CastContext}. To keep track of the session, {@link #isCastSessionAvailable()} can
 * be queried and {@link SessionAvailabilityListener} can be implemented and attached to the player.
 *
 * <p>If no session is available, the player state will remain unchanged and calls to methods that
 * alter it will be ignored. Querying the player state is possible even when no session is
 * available, in which case, the last observed receiver app state is reported.
 *
 * <p>Methods should be called on the application's main thread.
 */
@UnstableApi
public final class CastPlayer extends BasePlayer {
  /**
   * A {@link DeviceInfo#PLAYBACK_TYPE_REMOTE remote} {@link DeviceInfo} with a null {@link
   * DeviceInfo#routingControllerId}.
   */
  public static final DeviceInfo DEVICE_INFO_REMOTE_EMPTY =
      new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE).build();
  static {
    MediaLibraryInfo.registerModule("media3.cast");
  }
  @VisibleForTesting
  /* package */ static final Commands PERMANENT_AVAILABLE_COMMANDS =
      new Commands.Builder()
          .addAll(
              COMMAND_PLAY_PAUSE,
              COMMAND_PREPARE,
              COMMAND_STOP,
              COMMAND_SEEK_TO_DEFAULT_POSITION,
              COMMAND_SEEK_TO_MEDIA_ITEM,
              COMMAND_SET_REPEAT_MODE,
              COMMAND_SET_SPEED_AND_PITCH,
              COMMAND_GET_CURRENT_MEDIA_ITEM,
              COMMAND_GET_TIMELINE,
              COMMAND_GET_METADATA,
              COMMAND_SET_PLAYLIST_METADATA,
              COMMAND_SET_MEDIA_ITEM,
              COMMAND_CHANGE_MEDIA_ITEMS,
              COMMAND_GET_TRACKS,
              COMMAND_RELEASE)
          .build();
  public static final float MIN_SPEED_SUPPORTED = 0.5f;
  public static final float MAX_SPEED_SUPPORTED = 2.0f;
  private static final String TAG = "CastPlayer";
  private static final long PROGRESS_REPORT_PERIOD_MS = 1000;
  private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0];
  private final CastContext castContext;
  private final MediaItemConverter mediaItemConverter;
  private final long seekBackIncrementMs;
  private final long seekForwardIncrementMs;
  private final long maxSeekToPreviousPositionMs;
  // TODO: Allow custom implementations of CastTimelineTracker.
  private final CastTimelineTracker timelineTracker;
  private final Timeline.Period period;
  @Nullable private final Api30Impl api30Impl;
  // Result callbacks.
  private final StatusListener statusListener;
  private final SeekResultCallback seekResultCallback;
  // Listeners and notification.
  private final ListenerSet<Listener> listeners;
  @Nullable private SessionAvailabilityListener sessionAvailabilityListener;
  // Internal state.
  private final StateHolder<Boolean> playWhenReady;
  private final StateHolder<Integer> repeatMode;
  private final StateHolder<PlaybackParameters> playbackParameters;
  @Nullable private RemoteMediaClient remoteMediaClient;
  private CastTimeline currentTimeline;
  private Tracks currentTracks;
  private Commands availableCommands;
  private @Player.State int playbackState;
  private int currentWindowIndex;
  private long lastReportedPositionMs;
  private int pendingSeekCount;
  private int pendingSeekWindowIndex;
  private long pendingSeekPositionMs;
  @Nullable private PositionInfo pendingMediaItemRemovalPosition;
  private MediaMetadata mediaMetadata;
  private DeviceInfo deviceInfo;
  /**
   * Creates a new cast player.
   *
   * <p>The returned player uses a {@link DefaultMediaItemConverter} and
   *
   * <p>{@code mediaItemConverter} is set to a {@link DefaultMediaItemConverter}, {@code
   * seekBackIncrementMs} is set to {@link C#DEFAULT_SEEK_BACK_INCREMENT_MS} and {@code
   * seekForwardIncrementMs} is set to {@link C#DEFAULT_SEEK_FORWARD_INCREMENT_MS}.
   *
   * @param castContext The context from which the cast session is obtained.
   */
  public CastPlayer(CastContext castContext) {
    this(castContext, new DefaultMediaItemConverter());
  }
  /**
   * Creates a new cast player.
   *
   * <p>{@code seekBackIncrementMs} is set to {@link C#DEFAULT_SEEK_BACK_INCREMENT_MS} and {@code
   * seekForwardIncrementMs} is set to {@link C#DEFAULT_SEEK_FORWARD_INCREMENT_MS}.
   *
   * @param castContext The context from which the cast session is obtained.
   * @param mediaItemConverter The {@link MediaItemConverter} to use.
   */
  public CastPlayer(CastContext castContext, MediaItemConverter mediaItemConverter) {
    this(
        castContext,
        mediaItemConverter,
        C.DEFAULT_SEEK_BACK_INCREMENT_MS,
        C.DEFAULT_SEEK_FORWARD_INCREMENT_MS);
  }
  /**
   * Creates a new cast player.
   *
   * @param castContext The context from which the cast session is obtained.
   * @param mediaItemConverter The {@link MediaItemConverter} to use.
   * @param seekBackIncrementMs The {@link #seekBack()} increment, in milliseconds.
   * @param seekForwardIncrementMs The {@link #seekForward()} increment, in milliseconds.
   * @throws IllegalArgumentException If {@code seekBackIncrementMs} or {@code
   *     seekForwardIncrementMs} is non-positive.
   */
  public CastPlayer(
      CastContext castContext,
      MediaItemConverter mediaItemConverter,
      @IntRange(from = 1) long seekBackIncrementMs,
      @IntRange(from = 1) long seekForwardIncrementMs) {
    this(
        /* context= */ null,
        castContext,
        mediaItemConverter,
        seekBackIncrementMs,
        seekForwardIncrementMs,
        C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS);
  }
  /**
   * Creates a new cast player.
   *
   * @param context A {@link Context} used to populate {@link #getDeviceInfo()}. If null, {@link
   *     #getDeviceInfo()} will always return {@link #DEVICE_INFO_REMOTE_EMPTY}.
   * @param castContext The context from which the cast session is obtained.
   * @param mediaItemConverter The {@link MediaItemConverter} to use.
   * @param seekBackIncrementMs The {@link #seekBack()} increment, in milliseconds.
   * @param seekForwardIncrementMs The {@link #seekForward()} increment, in milliseconds.
   * @param maxSeekToPreviousPositionMs The maximum position for which {@link #seekToPrevious()}
   *     seeks to the previous {@link MediaItem}, in milliseconds.
   * @throws IllegalArgumentException If {@code seekBackIncrementMs} or {@code
   *     seekForwardIncrementMs} is non-positive, or if {@code maxSeekToPreviousPositionMs} is
   *     negative.
   */
  public CastPlayer(
      @Nullable Context context,
      CastContext castContext,
      MediaItemConverter mediaItemConverter,
      @IntRange(from = 1) long seekBackIncrementMs,
      @IntRange(from = 1) long seekForwardIncrementMs,
      @IntRange(from = 0) long maxSeekToPreviousPositionMs) {
    checkArgument(seekBackIncrementMs > 0 && seekForwardIncrementMs > 0);
    checkArgument(maxSeekToPreviousPositionMs >= 0L);
    this.castContext = castContext;
    this.mediaItemConverter = mediaItemConverter;
    this.seekBackIncrementMs = seekBackIncrementMs;
    this.seekForwardIncrementMs = seekForwardIncrementMs;
    this.maxSeekToPreviousPositionMs = maxSeekToPreviousPositionMs;
    timelineTracker = new CastTimelineTracker(mediaItemConverter);
    period = new Timeline.Period();
    statusListener = new StatusListener();
    seekResultCallback = new SeekResultCallback();
    listeners =
        new ListenerSet<>(
            Looper.getMainLooper(),
            Clock.DEFAULT,
            (listener, flags) -> listener.onEvents(/* player= */ this, new Events(flags)));
    playWhenReady = new StateHolder<>(false);
    repeatMode = new StateHolder<>(REPEAT_MODE_OFF);
    playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT);
    playbackState = STATE_IDLE;
    currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
    mediaMetadata = MediaMetadata.EMPTY;
    currentTracks = Tracks.EMPTY;
    availableCommands = new Commands.Builder().addAll(PERMANENT_AVAILABLE_COMMANDS).build();
    pendingSeekWindowIndex = C.INDEX_UNSET;
    pendingSeekPositionMs = C.TIME_UNSET;
    SessionManager sessionManager = castContext.getSessionManager();
    sessionManager.addSessionManagerListener(statusListener, CastSession.class);
    CastSession session = sessionManager.getCurrentCastSession();
    setRemoteMediaClient(session != null ? session.getRemoteMediaClient() : null);
    updateInternalStateAndNotifyIfChanged();
    if (SDK_INT >= 30 && context != null) {
      api30Impl = new Api30Impl(context);
      api30Impl.initialize();
      deviceInfo = api30Impl.fetchDeviceInfo();
    } else {
      api30Impl = null;
      deviceInfo = DEVICE_INFO_REMOTE_EMPTY;
    }
  }
  /**
   * Returns the item that corresponds to the period with the given id, or null if no media queue or
   * period with id {@code periodId} exist.
   *
   * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item
   *     to get.
   * @return The item that corresponds to the period with the given id, or null if no media queue or
   *     period with id {@code periodId} exist.
   */
  @Nullable
  public MediaQueueItem getItem(int periodId) {
    MediaStatus mediaStatus = getMediaStatus();
    return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
        ? mediaStatus.getItemById(periodId)
        : null;
  }
  // CastSession methods.
  /** Returns whether a cast session is available. */
  public boolean isCastSessionAvailable() {
    return remoteMediaClient != null;
  }
  /**
   * Sets a listener for updates on the cast session availability.
   *
   * @param listener The {@link SessionAvailabilityListener}, or null to clear the listener.
   */
  public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) {
    sessionAvailabilityListener = listener;
  }
  // Player implementation.
  @Override
  public Looper getApplicationLooper() {
    return Looper.getMainLooper();
  }
  @Override
  public void addListener(Listener listener) {
    listeners.add(listener);
  }
  @Override
  public void removeListener(Listener listener) {
    listeners.remove(listener);
  }
  @Override
  public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
    int mediaItemIndex = resetPosition ? 0 : getCurrentMediaItemIndex();
    long startPositionMs = resetPosition ? C.TIME_UNSET : getContentPosition();
    setMediaItems(mediaItems, mediaItemIndex, startPositionMs);
  }
  @Override
  public void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
    setMediaItemsInternal(mediaItems, startIndex, startPositionMs, repeatMode.value);
  }
  @Override
  public void addMediaItems(int index, List<MediaItem> mediaItems) {
    checkArgument(index >= 0);
    int uid = MediaQueueItem.INVALID_ITEM_ID;
    if (index < currentTimeline.getWindowCount()) {
      uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid;
    }
    addMediaItemsInternal(mediaItems, uid);
  }
  @Override
  public void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
    checkArgument(fromIndex >= 0 && fromIndex <= toIndex && newIndex >= 0);
    int playlistSize = currentTimeline.getWindowCount();
    toIndex = min(toIndex, playlistSize);
    newIndex = min(newIndex, playlistSize - (toIndex - fromIndex));
    if (fromIndex >= playlistSize || fromIndex == toIndex || fromIndex == newIndex) {
      // Do nothing.
      return;
    }
    int[] uids = new int[toIndex - fromIndex];
    for (int i = 0; i < uids.length; i++) {
      uids[i] = (int) currentTimeline.getWindow(/* windowIndex= */ i + fromIndex, window).uid;
    }
    moveMediaItemsInternal(uids, fromIndex, newIndex);
  }
  @Override
  public void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
    checkArgument(fromIndex >= 0 && fromIndex <= toIndex);
    int playlistSize = currentTimeline.getWindowCount();
    if (fromIndex > playlistSize) {
      return;
    }
    toIndex = min(toIndex, playlistSize);
    addMediaItems(toIndex, mediaItems);
    removeMediaItems(fromIndex, toIndex);
  }
  @Override
  public void removeMediaItems(int fromIndex, int toIndex) {
    checkArgument(fromIndex >= 0 && toIndex >= fromIndex);
    int playlistSize = currentTimeline.getWindowCount();
    toIndex = min(toIndex, playlistSize);
    if (fromIndex >= playlistSize || fromIndex == toIndex) {
      // Do nothing.
      return;
    }
    int[] uids = new int[toIndex - fromIndex];
    for (int i = 0; i < uids.length; i++) {
      uids[i] = (int) currentTimeline.getWindow(/* windowIndex= */ i + fromIndex, window).uid;
    }
    removeMediaItemsInternal(uids);
  }
  @Override
  public Commands getAvailableCommands() {
    return availableCommands;
  }
  @Override
  public void prepare() {
    // Do nothing.
  }
  @Override
  public @Player.State int getPlaybackState() {
    return playbackState;
  }
  @Override
  public @PlaybackSuppressionReason int getPlaybackSuppressionReason() {
    return Player.PLAYBACK_SUPPRESSION_REASON_NONE;
  }
  @Override
  @Nullable
  public PlaybackException getPlayerError() {
    return null;
  }
  @Override
  public void setPlayWhenReady(boolean playWhenReady) {
    if (remoteMediaClient == null) {
      return;
    }
    // We update the local state and send the message to the receiver app, which will cause the
    // operation to be perceived as synchronous by the user. When the operation reports a result,
    // the local state will be updated to reflect the state reported by the Cast SDK.
    setPlayerStateAndNotifyIfChanged(
        playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, playbackState);
    listeners.flushEvents();
    PendingResult<MediaChannelResult> pendingResult =
        playWhenReady ? remoteMediaClient.play() : remoteMediaClient.pause();
    this.playWhenReady.pendingResultCallback =
        new ResultCallback<MediaChannelResult>() {
          @Override
          public void onResult(MediaChannelResult mediaChannelResult) {
            if (remoteMediaClient != null) {
              updatePlayerStateAndNotifyIfChanged(this);
              listeners.flushEvents();
            }
          }
        };
    pendingResult.setResultCallback(this.playWhenReady.pendingResultCallback);
  }
  @Override
  public boolean getPlayWhenReady() {
    return playWhenReady.value;
  }
  // We still call Listener#onPositionDiscontinuity(@DiscontinuityReason int) for backwards
  // compatibility with listeners that don't implement
  // onPositionDiscontinuity(PositionInfo, PositionInfo, @DiscontinuityReason int).
  @SuppressWarnings("deprecation")
  @Override
  @VisibleForTesting(otherwise = PROTECTED)
  public void seekTo(
      int mediaItemIndex,
      long positionMs,
      @Player.Command int seekCommand,
      boolean isRepeatingCurrentItem) {
    if (mediaItemIndex == C.INDEX_UNSET) {
      return;
    }
    checkArgument(mediaItemIndex >= 0);
    if (!currentTimeline.isEmpty() && mediaItemIndex >= currentTimeline.getWindowCount()) {
      return;
    }
    MediaStatus mediaStatus = getMediaStatus();
    // We assume the default position is 0. There is no support for seeking to the default position
    // in RemoteMediaClient.
    positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
    if (mediaStatus != null) {
      if (getCurrentMediaItemIndex() != mediaItemIndex) {
        remoteMediaClient
            .queueJumpToItem(
                (int) currentTimeline.getPeriod(mediaItemIndex, period).uid, positionMs, null)
            .setResultCallback(seekResultCallback);
      } else {
        remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback);
      }
      PositionInfo oldPosition = getCurrentPositionInfo();
      pendingSeekCount++;
      pendingSeekWindowIndex = mediaItemIndex;
      pendingSeekPositionMs = positionMs;
      PositionInfo newPosition = getCurrentPositionInfo();
      listeners.queueEvent(
          Player.EVENT_POSITION_DISCONTINUITY,
          listener -> {
            listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK);
            listener.onPositionDiscontinuity(oldPosition, newPosition, DISCONTINUITY_REASON_SEEK);
          });
      if (oldPosition.mediaItemIndex != newPosition.mediaItemIndex) {
        // TODO(internal b/182261884): queue `onMediaItemTransition` event when the media item is
        // repeated.
        MediaItem mediaItem = getCurrentTimeline().getWindow(mediaItemIndex, window).mediaItem;
        listeners.queueEvent(
            Player.EVENT_MEDIA_ITEM_TRANSITION,
            listener ->
                listener.onMediaItemTransition(mediaItem, MEDIA_ITEM_TRANSITION_REASON_SEEK));
        MediaMetadata oldMediaMetadata = mediaMetadata;
        mediaMetadata = getMediaMetadataInternal();
        if (!oldMediaMetadata.equals(mediaMetadata)) {
          listeners.queueEvent(
              Player.EVENT_MEDIA_METADATA_CHANGED,
              listener -> listener.onMediaMetadataChanged(mediaMetadata));
        }
      }
      updateAvailableCommandsAndNotifyIfChanged();
    }
    listeners.flushEvents();
  }
  @Override
  public long getSeekBackIncrement() {
    return seekBackIncrementMs;
  }
  @Override
  public long getSeekForwardIncrement() {
    return seekForwardIncrementMs;
  }
  @Override
  public long getMaxSeekToPreviousPosition() {
    return maxSeekToPreviousPositionMs;
  }
  @Override
  public PlaybackParameters getPlaybackParameters() {
    return playbackParameters.value;
  }
  @Override
  public void stop() {
    playbackState = STATE_IDLE;
    if (remoteMediaClient != null) {
      // TODO(b/69792021): Support or emulate stop without position reset.
      remoteMediaClient.stop();
    }
  }
  @Override
  public void release() {
    // The SDK_INT check is not necessary, but it prevents a lint error for the release call.
    if (SDK_INT >= 30 && api30Impl != null) {
      api30Impl.release();
    }
    SessionManager sessionManager = castContext.getSessionManager();
    sessionManager.removeSessionManagerListener(statusListener, CastSession.class);
    sessionManager.endCurrentSession(false);
  }
  @Override
  public void setPlaybackParameters(PlaybackParameters playbackParameters) {
    if (remoteMediaClient == null) {
      return;
    }
    PlaybackParameters actualPlaybackParameters =
        new PlaybackParameters(
            Util.constrainValue(
                playbackParameters.speed, MIN_SPEED_SUPPORTED, MAX_SPEED_SUPPORTED));
    setPlaybackParametersAndNotifyIfChanged(actualPlaybackParameters);
    listeners.flushEvents();
    PendingResult<MediaChannelResult> pendingResult =
        remoteMediaClient.setPlaybackRate(actualPlaybackParameters.speed, /* customData= */ null);
    this.playbackParameters.pendingResultCallback =
        new ResultCallback<MediaChannelResult>() {
          @Override
          public void onResult(MediaChannelResult mediaChannelResult) {
            if (remoteMediaClient != null) {
              updatePlaybackRateAndNotifyIfChanged(this);
              listeners.flushEvents();
            }
          }
        };
    pendingResult.setResultCallback(this.playbackParameters.pendingResultCallback);
  }
  @Override
  public void setRepeatMode(@RepeatMode int repeatMode) {
    if (remoteMediaClient == null) {
      return;
    }
    // We update the local state and send the message to the receiver app, which will cause the
    // operation to be perceived as synchronous by the user. When the operation reports a result,
    // the local state will be updated to reflect the state reported by the Cast SDK.
    setRepeatModeAndNotifyIfChanged(repeatMode);
    listeners.flushEvents();
    PendingResult<MediaChannelResult> pendingResult =
        remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), /* customData= */ null);
    this.repeatMode.pendingResultCallback =
        new ResultCallback<MediaChannelResult>() {
          @Override
          public void onResult(MediaChannelResult mediaChannelResult) {
            if (remoteMediaClient != null) {
              updateRepeatModeAndNotifyIfChanged(this);
              listeners.flushEvents();
            }
          }
        };
    pendingResult.setResultCallback(this.repeatMode.pendingResultCallback);
  }
  @Override
  public @RepeatMode int getRepeatMode() {
    return repeatMode.value;
  }
  @Override
  public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
    // TODO: Support shuffle mode.
  }
  @Override
  public boolean getShuffleModeEnabled() {
    // TODO: Support shuffle mode.
    return false;
  }
  @Override
  public Tracks getCurrentTracks() {
    return currentTracks;
  }
  @Override
  public TrackSelectionParameters getTrackSelectionParameters() {
    return TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT;
  }
  @Override
  public void setTrackSelectionParameters(TrackSelectionParameters parameters) {}
  @Override
  public MediaMetadata getMediaMetadata() {
    return mediaMetadata;
  }
  public MediaMetadata getMediaMetadataInternal() {
    MediaItem currentMediaItem = getCurrentMediaItem();
    return currentMediaItem != null ? currentMediaItem.mediaMetadata : MediaMetadata.EMPTY;
  }
  @Override
  public MediaMetadata getPlaylistMetadata() {
    // CastPlayer does not currently support metadata.
    return MediaMetadata.EMPTY;
  }
  /** This method is not supported and does nothing. */
  @Override
  public void setPlaylistMetadata(MediaMetadata mediaMetadata) {
    // CastPlayer does not currently support metadata.
  }
  @Override
  public Timeline getCurrentTimeline() {
    return currentTimeline;
  }
  @Override
  public int getCurrentPeriodIndex() {
    return getCurrentMediaItemIndex();
  }
  @Override
  public int getCurrentMediaItemIndex() {
    return pendingSeekWindowIndex != C.INDEX_UNSET ? pendingSeekWindowIndex : currentWindowIndex;
  }
  // TODO: Fill the cast timeline information with ProgressListener's duration updates.
  // See [Internal: b/65152553].
  @Override
  public long getDuration() {
    return getContentDuration();
  }
  @Override
  public long getCurrentPosition() {
    return pendingSeekPositionMs != C.TIME_UNSET
        ? pendingSeekPositionMs
        : remoteMediaClient != null
            ? remoteMediaClient.getApproximateStreamPosition()
            : lastReportedPositionMs;
  }
  @Override
  public long getBufferedPosition() {
    return getCurrentPosition();
  }
  @Override
  public long getTotalBufferedDuration() {
    long bufferedPosition = getBufferedPosition();
    long currentPosition = getCurrentPosition();
    return bufferedPosition == C.TIME_UNSET || currentPosition == C.TIME_UNSET
        ? 0
        : bufferedPosition - currentPosition;
  }
  @Override
  public boolean isPlayingAd() {
    return false;
  }
  @Override
  public int getCurrentAdGroupIndex() {
    return C.INDEX_UNSET;
  }
  @Override
  public int getCurrentAdIndexInAdGroup() {
    return C.INDEX_UNSET;
  }
  @Override
  public boolean isLoading() {
    return false;
  }
  @Override
  public long getContentPosition() {
    return getCurrentPosition();
  }
  @Override
  public long getContentBufferedPosition() {
    return getBufferedPosition();
  }
  /** This method is not supported and returns {@link AudioAttributes#DEFAULT}. */
  @Override
  public AudioAttributes getAudioAttributes() {
    return AudioAttributes.DEFAULT;
  }
  /** This method is not supported and does nothing. */
  @Override
  public void setVolume(float volume) {}
  /** This method is not supported and returns 1. */
  @Override
  public float getVolume() {
    return 1;
  }
  /** This method is not supported and does nothing. */
  @Override
  public void clearVideoSurface() {}
  /** This method is not supported and does nothing. */
  @Override
  public void clearVideoSurface(@Nullable Surface surface) {}
  /** This method is not supported and does nothing. */
  @Override
  public void setVideoSurface(@Nullable Surface surface) {}
  /** This method is not supported and does nothing. */
  @Override
  public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {}
  /** This method is not supported and does nothing. */
  @Override
  public void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {}
  /** This method is not supported and does nothing. */
  @Override
  public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) {}
  /** This method is not supported and does nothing. */
  @Override
  public void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) {}
  /** This method is not supported and does nothing. */
  @Override
  public void setVideoTextureView(@Nullable TextureView textureView) {}
  /** This method is not supported and does nothing. */
  @Override
  public void clearVideoTextureView(@Nullable TextureView textureView) {}
  /** This method is not supported and returns {@link VideoSize#UNKNOWN}. */
  @Override
  public VideoSize getVideoSize() {
    return VideoSize.UNKNOWN;
  }
  /** This method is not supported and returns {@link Size#UNKNOWN}. */
  @Override
  public Size getSurfaceSize() {
    return Size.UNKNOWN;
  }
  /** This method is not supported and returns an empty {@link CueGroup}. */
  @Override
  public CueGroup getCurrentCues() {
    return CueGroup.EMPTY_TIME_ZERO;
  }
  /**
   * Returns a {@link DeviceInfo} describing the receiver device. Returns {@link
   * #DEVICE_INFO_REMOTE_EMPTY} if no {@link Context} was provided at construction, or if the Cast
   * {@link RoutingController} could not be identified.
   */
  @Override
  public DeviceInfo getDeviceInfo() {
    return deviceInfo;
  }
  /** This method is not supported and always returns {@code 0}. */
  @Override
  public int getDeviceVolume() {
    return 0;
  }
  /** This method is not supported and always returns {@code false}. */
  @Override
  public boolean isDeviceMuted() {
    return false;
  }
  /**
   * @deprecated Use {@link #setDeviceVolume(int, int)} instead.
   */
  @Deprecated
  @Override
  public void setDeviceVolume(int volume) {}
  /** This method is not supported and does nothing. */
  @Override
  public void setDeviceVolume(int volume, @C.VolumeFlags int flags) {}
  /**
   * @deprecated Use {@link #increaseDeviceVolume(int)} instead.
   */
  @Deprecated
  @Override
  public void increaseDeviceVolume() {}
  /** This method is not supported and does nothing. */
  @Override
  public void increaseDeviceVolume(@C.VolumeFlags int flags) {}
  /**
   * @deprecated Use {@link #decreaseDeviceVolume(int)} instead.
   */
  @Deprecated
  @Override
  public void decreaseDeviceVolume() {}
  /** This method is not supported and does nothing. */
  @Override
  public void decreaseDeviceVolume(@C.VolumeFlags int flags) {}
  /**
   * @deprecated Use {@link #setDeviceMuted(boolean, int)} instead.
   */
  @Deprecated
  @Override
  public void setDeviceMuted(boolean muted) {}
  /** This method is not supported and does nothing. */
  @Override
  public void setDeviceMuted(boolean muted, @C.VolumeFlags int flags) {}
  /** This method is not supported and does nothing. */
  @Override
  public void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus) {}
  // Internal methods.
  // Call deprecated callbacks.
  @SuppressWarnings("deprecation")
  private void updateInternalStateAndNotifyIfChanged() {
    if (remoteMediaClient == null) {
      // There is no session. We leave the state of the player as it is now.
      return;
    }
    int oldWindowIndex = this.currentWindowIndex;
    MediaMetadata oldMediaMetadata = mediaMetadata;
    @Nullable
    Object oldPeriodUid =
        !getCurrentTimeline().isEmpty()
            ? getCurrentTimeline().getPeriod(oldWindowIndex, period, /* setIds= */ true).uid
            : null;
    updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
    updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
    updatePlaybackRateAndNotifyIfChanged(/* resultCallback= */ null);
    boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged();
    Timeline currentTimeline = getCurrentTimeline();
    currentWindowIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline);
    mediaMetadata = getMediaMetadataInternal();
    @Nullable
    Object currentPeriodUid =
        !currentTimeline.isEmpty()
            ? currentTimeline.getPeriod(currentWindowIndex, period, /* setIds= */ true).uid
            : null;
    if (!playingPeriodChangedByTimelineChange
        && !Util.areEqual(oldPeriodUid, currentPeriodUid)
        && pendingSeekCount == 0) {
      // Report discontinuity and media item auto transition.
      currentTimeline.getPeriod(oldWindowIndex, period, /* setIds= */ true);
      currentTimeline.getWindow(oldWindowIndex, window);
      long windowDurationMs = window.getDurationMs();
      PositionInfo oldPosition =
          new PositionInfo(
              window.uid,
              period.windowIndex,
              window.mediaItem,
              period.uid,
              period.windowIndex,
              /* positionMs= */ windowDurationMs,
              /* contentPositionMs= */ windowDurationMs,
              /* adGroupIndex= */ C.INDEX_UNSET,
              /* adIndexInAdGroup= */ C.INDEX_UNSET);
      currentTimeline.getPeriod(currentWindowIndex, period, /* setIds= */ true);
      currentTimeline.getWindow(currentWindowIndex, window);
      PositionInfo newPosition =
          new PositionInfo(
              window.uid,
              period.windowIndex,
              window.mediaItem,
              period.uid,
              period.windowIndex,
              /* positionMs= */ window.getDefaultPositionMs(),
              /* contentPositionMs= */ window.getDefaultPositionMs(),
              /* adGroupIndex= */ C.INDEX_UNSET,
              /* adIndexInAdGroup= */ C.INDEX_UNSET);
      listeners.queueEvent(
          Player.EVENT_POSITION_DISCONTINUITY,
          listener -> {
            listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AUTO_TRANSITION);
            listener.onPositionDiscontinuity(
                oldPosition, newPosition, DISCONTINUITY_REASON_AUTO_TRANSITION);
          });
      listeners.queueEvent(
          Player.EVENT_MEDIA_ITEM_TRANSITION,
          listener ->
              listener.onMediaItemTransition(
                  getCurrentMediaItem(), MEDIA_ITEM_TRANSITION_REASON_AUTO));
    }
    if (updateTracksAndSelectionsAndNotifyIfChanged()) {
      listeners.queueEvent(
          Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(currentTracks));
    }
    if (!oldMediaMetadata.equals(mediaMetadata)) {
      listeners.queueEvent(
          Player.EVENT_MEDIA_METADATA_CHANGED,
          listener -> listener.onMediaMetadataChanged(mediaMetadata));
    }
    updateAvailableCommandsAndNotifyIfChanged();
    listeners.flushEvents();
  }
  /**
   * Updates {@link #playWhenReady} and {@link #playbackState} to match the Cast {@code
   * remoteMediaClient} state, and notifies listeners of any state changes.
   *
   * <p>This method will only update values whose {@link StateHolder#pendingResultCallback} matches
   * the given {@code resultCallback}.
   */
  @RequiresNonNull("remoteMediaClient")
  private void updatePlayerStateAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
    boolean newPlayWhenReadyValue = playWhenReady.value;
    if (playWhenReady.acceptsUpdate(resultCallback)) {
      newPlayWhenReadyValue = !remoteMediaClient.isPaused();
      playWhenReady.clearPendingResultCallback();
    }
    @PlayWhenReadyChangeReason
    int playWhenReadyChangeReason =
        newPlayWhenReadyValue != playWhenReady.value
            ? PLAY_WHEN_READY_CHANGE_REASON_REMOTE
            : PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
    // We do not mask the playback state, so try setting it regardless of the playWhenReady masking.
    setPlayerStateAndNotifyIfChanged(
        newPlayWhenReadyValue, playWhenReadyChangeReason, fetchPlaybackState(remoteMediaClient));
  }
  @RequiresNonNull("remoteMediaClient")
  private void updatePlaybackRateAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
    if (playbackParameters.acceptsUpdate(resultCallback)) {
      @Nullable MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
      float speed =
          mediaStatus != null
              ? (float) mediaStatus.getPlaybackRate()
              : PlaybackParameters.DEFAULT.speed;
      if (speed > 0.0f) {
        // Set the speed if not paused.
        setPlaybackParametersAndNotifyIfChanged(new PlaybackParameters(speed));
      }
      playbackParameters.clearPendingResultCallback();
    }
  }
  @RequiresNonNull("remoteMediaClient")
  private void updateRepeatModeAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
    if (repeatMode.acceptsUpdate(resultCallback)) {
      setRepeatModeAndNotifyIfChanged(fetchRepeatMode(remoteMediaClient));
      repeatMode.clearPendingResultCallback();
    }
  }
  /**
   * Updates the timeline and notifies {@link Player.Listener event listeners} if required.
   *
   * @return Whether the timeline change has caused a change of the period currently being played.
   */
  @SuppressWarnings("deprecation") // Calling deprecated listener method.
  private boolean updateTimelineAndNotifyIfChanged() {
    Timeline oldTimeline = currentTimeline;
    int oldWindowIndex = currentWindowIndex;
    boolean playingPeriodChanged = false;
    if (updateTimeline()) {
      // TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
      //     TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
      Timeline timeline = currentTimeline;
      // Call onTimelineChanged.
      listeners.queueEvent(
          Player.EVENT_TIMELINE_CHANGED,
          listener ->
              listener.onTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
      // Call onPositionDiscontinuity if required.
      Timeline currentTimeline = getCurrentTimeline();
      boolean playingPeriodRemoved = false;
      if (!oldTimeline.isEmpty()) {
        Object oldPeriodUid =
            castNonNull(oldTimeline.getPeriod(oldWindowIndex, period, /* setIds= */ true).uid);
        playingPeriodRemoved = currentTimeline.getIndexOfPeriod(oldPeriodUid) == C.INDEX_UNSET;
      }
      if (playingPeriodRemoved) {
        PositionInfo oldPosition;
        if (pendingMediaItemRemovalPosition != null) {
          oldPosition = pendingMediaItemRemovalPosition;
          pendingMediaItemRemovalPosition = null;
        } else {
          // If the media item has been removed by another client, we don't know the removal
          // position. We use the current position as a fallback.
          oldTimeline.getPeriod(oldWindowIndex, period, /* setIds= */ true);
          oldTimeline.getWindow(period.windowIndex, window);
          oldPosition =
              new PositionInfo(
                  window.uid,
                  period.windowIndex,
                  window.mediaItem,
                  period.uid,
                  period.windowIndex,
                  getCurrentPosition(),
                  getContentPosition(),
                  /* adGroupIndex= */ C.INDEX_UNSET,
                  /* adIndexInAdGroup= */ C.INDEX_UNSET);
        }
        PositionInfo newPosition = getCurrentPositionInfo();
        listeners.queueEvent(
            Player.EVENT_POSITION_DISCONTINUITY,
            listener -> {
              listener.onPositionDiscontinuity(DISCONTINUITY_REASON_REMOVE);
              listener.onPositionDiscontinuity(
                  oldPosition, newPosition, DISCONTINUITY_REASON_REMOVE);
            });
      }
      // Call onMediaItemTransition if required.
      playingPeriodChanged =
          currentTimeline.isEmpty() != oldTimeline.isEmpty() || playingPeriodRemoved;
      if (playingPeriodChanged) {
        listeners.queueEvent(
            Player.EVENT_MEDIA_ITEM_TRANSITION,
            listener ->
                listener.onMediaItemTransition(
                    getCurrentMediaItem(), MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
      }
      updateAvailableCommandsAndNotifyIfChanged();
    }
    return playingPeriodChanged;
  }
  /**
   * Updates the current timeline. The current window index may change as a result.
   *
   * @return Whether the current timeline has changed.
   */
  private boolean updateTimeline() {
    CastTimeline oldTimeline = currentTimeline;
    MediaStatus status = getMediaStatus();
    currentTimeline =
        status != null
            ? timelineTracker.getCastTimeline(remoteMediaClient)
            : CastTimeline.EMPTY_CAST_TIMELINE;
    boolean timelineChanged = !oldTimeline.equals(currentTimeline);
    if (timelineChanged) {
      currentWindowIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline);
    }
    return timelineChanged;
  }
  /** Updates the internal tracks and selection and returns whether they have changed. */
  private boolean updateTracksAndSelectionsAndNotifyIfChanged() {
    if (remoteMediaClient == null) {
      // There is no session. We leave the state of the player as it is now.
      return false;
    }
    @Nullable MediaStatus mediaStatus = getMediaStatus();
    @Nullable MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null;
    @Nullable
    List<MediaTrack> castMediaTracks = mediaInfo != null ? mediaInfo.getMediaTracks() : null;
    if (castMediaTracks == null || castMediaTracks.isEmpty()) {
      boolean hasChanged = !Tracks.EMPTY.equals(currentTracks);
      currentTracks = Tracks.EMPTY;
      return hasChanged;
    }
    @Nullable long[] activeTrackIds = mediaStatus.getActiveTrackIds();
    if (activeTrackIds == null) {
      activeTrackIds = EMPTY_TRACK_ID_ARRAY;
    }
    Tracks.Group[] trackGroups = new Tracks.Group[castMediaTracks.size()];
    for (int i = 0; i < castMediaTracks.size(); i++) {
      MediaTrack mediaTrack = castMediaTracks.get(i);
      TrackGroup trackGroup =
          new TrackGroup(/* id= */ Integer.toString(i), CastUtils.mediaTrackToFormat(mediaTrack));
      @C.FormatSupport int[] trackSupport = new int[] {C.FORMAT_HANDLED};
      boolean[] trackSelected = new boolean[] {isTrackActive(mediaTrack.getId(), activeTrackIds)};
      trackGroups[i] =
          new Tracks.Group(trackGroup, /* adaptiveSupported= */ false, trackSupport, trackSelected);
    }
    Tracks newTracks = new Tracks(ImmutableList.copyOf(trackGroups));
    if (!newTracks.equals(currentTracks)) {
      currentTracks = newTracks;
      return true;
    }
    return false;
  }
  private void updateAvailableCommandsAndNotifyIfChanged() {
    Commands previousAvailableCommands = availableCommands;
    availableCommands = Util.getAvailableCommands(/* player= */ this, PERMANENT_AVAILABLE_COMMANDS);
    if (!availableCommands.equals(previousAvailableCommands)) {
      listeners.queueEvent(
          Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
          listener -> listener.onAvailableCommandsChanged(availableCommands));
    }
  }
  private void setMediaItemsInternal(
      List<MediaItem> mediaItems,
      int startIndex,
      long startPositionMs,
      @RepeatMode int repeatMode) {
    if (remoteMediaClient == null || mediaItems.isEmpty()) {
      return;
    }
    startPositionMs = startPositionMs == C.TIME_UNSET ? 0 : startPositionMs;
    if (startIndex == C.INDEX_UNSET) {
      startIndex = getCurrentMediaItemIndex();
      startPositionMs = getCurrentPosition();
    }
    Timeline currentTimeline = getCurrentTimeline();
    if (!currentTimeline.isEmpty()) {
      pendingMediaItemRemovalPosition = getCurrentPositionInfo();
    }
    MediaQueueItem[] mediaQueueItems = toMediaQueueItems(mediaItems);
    timelineTracker.onMediaItemsSet(mediaItems, mediaQueueItems);
    remoteMediaClient.queueLoad(
        mediaQueueItems,
        min(startIndex, mediaItems.size() - 1),
        getCastRepeatMode(repeatMode),
        startPositionMs,
        /* customData= */ null);
  }
  private void addMediaItemsInternal(List<MediaItem> mediaItems, int uid) {
    if (remoteMediaClient == null || getMediaStatus() == null) {
      return;
    }
    MediaQueueItem[] itemsToInsert = toMediaQueueItems(mediaItems);
    timelineTracker.onMediaItemsAdded(mediaItems, itemsToInsert);
    remoteMediaClient.queueInsertItems(itemsToInsert, uid, /* customData= */ null);
  }
  private void moveMediaItemsInternal(int[] uids, int fromIndex, int newIndex) {
    if (remoteMediaClient == null || getMediaStatus() == null) {
      return;
    }
    int insertBeforeIndex = fromIndex < newIndex ? newIndex + uids.length : newIndex;
    int insertBeforeItemId = MediaQueueItem.INVALID_ITEM_ID;
    if (insertBeforeIndex < currentTimeline.getWindowCount()) {
      insertBeforeItemId = (int) currentTimeline.getWindow(insertBeforeIndex, window).uid;
    }
    remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null);
  }
  @Nullable
  private PendingResult<MediaChannelResult> removeMediaItemsInternal(int[] uids) {
    if (remoteMediaClient == null || getMediaStatus() == null) {
      return null;
    }
    Timeline timeline = getCurrentTimeline();
    if (!timeline.isEmpty()) {
      Object periodUid =
          castNonNull(timeline.getPeriod(getCurrentPeriodIndex(), period, /* setIds= */ true).uid);
      for (int uid : uids) {
        if (periodUid.equals(uid)) {
          pendingMediaItemRemovalPosition = getCurrentPositionInfo();
          break;
        }
      }
    }
    return remoteMediaClient.queueRemoveItems(uids, /* customData= */ null);
  }
  private PositionInfo getCurrentPositionInfo() {
    Timeline currentTimeline = getCurrentTimeline();
    @Nullable Object newPeriodUid = null;
    @Nullable Object newWindowUid = null;
    @Nullable MediaItem newMediaItem = null;
    if (!currentTimeline.isEmpty()) {
      newPeriodUid =
          currentTimeline.getPeriod(getCurrentPeriodIndex(), period, /* setIds= */ true).uid;
      newWindowUid = currentTimeline.getWindow(period.windowIndex, window).uid;
      newMediaItem = window.mediaItem;
    }
    return new PositionInfo(
        newWindowUid,
        getCurrentMediaItemIndex(),
        newMediaItem,
        newPeriodUid,
        getCurrentPeriodIndex(),
        getCurrentPosition(),
        getContentPosition(),
        /* adGroupIndex= */ C.INDEX_UNSET,
        /* adIndexInAdGroup= */ C.INDEX_UNSET);
  }
  private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) {
    if (this.repeatMode.value != repeatMode) {
      this.repeatMode.value = repeatMode;
      listeners.queueEvent(
          Player.EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode));
      updateAvailableCommandsAndNotifyIfChanged();
    }
  }
  private void setPlaybackParametersAndNotifyIfChanged(PlaybackParameters playbackParameters) {
    if (this.playbackParameters.value.equals(playbackParameters)) {
      return;
    }
    this.playbackParameters.value = playbackParameters;
    listeners.queueEvent(
        Player.EVENT_PLAYBACK_PARAMETERS_CHANGED,
        listener -> listener.onPlaybackParametersChanged(playbackParameters));
    updateAvailableCommandsAndNotifyIfChanged();
  }
  @SuppressWarnings("deprecation")
  private void setPlayerStateAndNotifyIfChanged(
      boolean playWhenReady,
      @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason,
      @Player.State int playbackState) {
    boolean wasPlaying = this.playbackState == Player.STATE_READY && this.playWhenReady.value;
    boolean playWhenReadyChanged = this.playWhenReady.value != playWhenReady;
    boolean playbackStateChanged = this.playbackState != playbackState;
    if (playWhenReadyChanged || playbackStateChanged) {
      this.playbackState = playbackState;
      this.playWhenReady.value = playWhenReady;
      listeners.queueEvent(
          /* eventFlag= */ C.INDEX_UNSET,
          listener -> listener.onPlayerStateChanged(playWhenReady, playbackState));
      if (playbackStateChanged) {
        listeners.queueEvent(
            Player.EVENT_PLAYBACK_STATE_CHANGED,
            listener -> listener.onPlaybackStateChanged(playbackState));
      }
      if (playWhenReadyChanged) {
        listeners.queueEvent(
            Player.EVENT_PLAY_WHEN_READY_CHANGED,
            listener -> listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason));
      }
      boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady;
      if (wasPlaying != isPlaying) {
        listeners.queueEvent(
            Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying));
      }
    }
  }
  private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) {
    if (this.remoteMediaClient == remoteMediaClient) {
      // Do nothing.
      return;
    }
    if (this.remoteMediaClient != null) {
      this.remoteMediaClient.unregisterCallback(statusListener);
      this.remoteMediaClient.removeProgressListener(statusListener);
    }
    this.remoteMediaClient = remoteMediaClient;
    if (remoteMediaClient != null) {
      if (sessionAvailabilityListener != null) {
        sessionAvailabilityListener.onCastSessionAvailable();
      }
      remoteMediaClient.registerCallback(statusListener);
      remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
      updateInternalStateAndNotifyIfChanged();
    } else if (sessionAvailabilityListener != null) {
      sessionAvailabilityListener.onCastSessionUnavailable();
    }
  }
  @Nullable
  private MediaStatus getMediaStatus() {
    return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
  }
  /**
   * Retrieves the playback state from {@code remoteMediaClient} and maps it into a {@link Player}
   * state
   */
  private static int fetchPlaybackState(RemoteMediaClient remoteMediaClient) {
    int receiverAppStatus = remoteMediaClient.getPlayerState();
    switch (receiverAppStatus) {
      case MediaStatus.PLAYER_STATE_BUFFERING:
      case MediaStatus.PLAYER_STATE_LOADING:
        return STATE_BUFFERING;
      case MediaStatus.PLAYER_STATE_PLAYING:
      case MediaStatus.PLAYER_STATE_PAUSED:
        return STATE_READY;
      case MediaStatus.PLAYER_STATE_IDLE:
      case MediaStatus.PLAYER_STATE_UNKNOWN:
      default:
        return STATE_IDLE;
    }
  }
  /**
   * Retrieves the repeat mode from {@code remoteMediaClient} and maps it into a {@link
   * Player.RepeatMode}.
   */
  private static @RepeatMode int fetchRepeatMode(RemoteMediaClient remoteMediaClient) {
    MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
    if (mediaStatus == null) {
      // No media session active, yet.
      return REPEAT_MODE_OFF;
    }
    int castRepeatMode = mediaStatus.getQueueRepeatMode();
    switch (castRepeatMode) {
      case MediaStatus.REPEAT_MODE_REPEAT_SINGLE:
        return REPEAT_MODE_ONE;
      case MediaStatus.REPEAT_MODE_REPEAT_ALL:
      case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE:
        return REPEAT_MODE_ALL;
      case MediaStatus.REPEAT_MODE_REPEAT_OFF:
        return REPEAT_MODE_OFF;
      default:
        throw new IllegalStateException();
    }
  }
  private static int fetchCurrentWindowIndex(
      @Nullable RemoteMediaClient remoteMediaClient, Timeline timeline) {
    if (remoteMediaClient == null) {
      return 0;
    }
    int currentWindowIndex = C.INDEX_UNSET;
    @Nullable MediaQueueItem currentItem = remoteMediaClient.getCurrentItem();
    if (currentItem != null) {
      currentWindowIndex = timeline.getIndexOfPeriod(currentItem.getItemId());
    }
    if (currentWindowIndex == C.INDEX_UNSET) {
      // The timeline is empty. Fall back to index 0.
      currentWindowIndex = 0;
    }
    return currentWindowIndex;
  }
  private static boolean isTrackActive(long id, long[] activeTrackIds) {
    for (long activeTrackId : activeTrackIds) {
      if (activeTrackId == id) {
        return true;
      }
    }
    return false;
  }
  @SuppressWarnings("VisibleForTests")
  private static int getCastRepeatMode(@RepeatMode int repeatMode) {
    switch (repeatMode) {
      case REPEAT_MODE_ONE:
        return MediaStatus.REPEAT_MODE_REPEAT_SINGLE;
      case REPEAT_MODE_ALL:
        return MediaStatus.REPEAT_MODE_REPEAT_ALL;
      case REPEAT_MODE_OFF:
        return MediaStatus.REPEAT_MODE_REPEAT_OFF;
      default:
        throw new IllegalArgumentException();
    }
  }
  private MediaQueueItem[] toMediaQueueItems(List<MediaItem> mediaItems) {
    MediaQueueItem[] mediaQueueItems = new MediaQueueItem[mediaItems.size()];
    for (int i = 0; i < mediaItems.size(); i++) {
      mediaQueueItems[i] = mediaItemConverter.toMediaQueueItem(mediaItems.get(i));
    }
    return mediaQueueItems;
  }
  // Internal classes.
  private final class StatusListener extends RemoteMediaClient.Callback
      implements SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener {
    // RemoteMediaClient.ProgressListener implementation.
    @Override
    public void onProgressUpdated(long progressMs, long unusedDurationMs) {
      lastReportedPositionMs = progressMs;
    }
    // RemoteMediaClient.Callback implementation.
    @Override
    public void onStatusUpdated() {
      updateInternalStateAndNotifyIfChanged();
    }
    @Override
    public void onMetadataUpdated() {}
    @Override
    public void onQueueStatusUpdated() {
      updateTimelineAndNotifyIfChanged();
      listeners.flushEvents();
    }
    @Override
    public void onPreloadStatusUpdated() {}
    @Override
    public void onSendingRemoteMediaRequest() {}
    @Override
    public void onAdBreakStatusUpdated() {}
    // SessionManagerListener implementation.
    @Override
    public void onSessionStarted(CastSession castSession, String s) {
      setRemoteMediaClient(castSession.getRemoteMediaClient());
    }
    @Override
    public void onSessionResumed(CastSession castSession, boolean b) {
      setRemoteMediaClient(castSession.getRemoteMediaClient());
    }
    @Override
    public void onSessionEnded(CastSession castSession, int i) {
      setRemoteMediaClient(null);
    }
    @Override
    public void onSessionSuspended(CastSession castSession, int i) {
      setRemoteMediaClient(null);
    }
    @Override
    public void onSessionResumeFailed(CastSession castSession, int statusCode) {
      Log.e(
          TAG,
          "Session resume failed. Error code "
              + statusCode
              + ": "
              + CastUtils.getLogString(statusCode));
    }
    @Override
    public void onSessionStarting(CastSession castSession) {
      // Do nothing.
    }
    @Override
    public void onSessionStartFailed(CastSession castSession, int statusCode) {
      Log.e(
          TAG,
          "Session start failed. Error code "
              + statusCode
              + ": "
              + CastUtils.getLogString(statusCode));
    }
    @Override
    public void onSessionEnding(CastSession castSession) {
      // Do nothing.
    }
    @Override
    public void onSessionResuming(CastSession castSession, String s) {
      // Do nothing.
    }
  }
  private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
    @Override
    public void onResult(MediaChannelResult result) {
      int statusCode = result.getStatus().getStatusCode();
      if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) {
        Log.e(
            TAG,
            "Seek failed. Error code " + statusCode + ": " + CastUtils.getLogString(statusCode));
      }
      if (--pendingSeekCount == 0) {
        currentWindowIndex = pendingSeekWindowIndex;
        pendingSeekWindowIndex = C.INDEX_UNSET;
        pendingSeekPositionMs = C.TIME_UNSET;
      }
    }
  }
  /** Holds the value and the masking status of a specific part of the {@link CastPlayer} state. */
  private static final class StateHolder<T> {
    /** The user-facing value of a specific part of the {@link CastPlayer} state. */
    public T value;
    /**
     * If {@link #value} is being masked, holds the result callback for the operation that triggered
     * the masking. Or null if {@link #value} is not being masked.
     */
    @Nullable public ResultCallback<MediaChannelResult> pendingResultCallback;
    public StateHolder(T initialValue) {
      value = initialValue;
    }
    public void clearPendingResultCallback() {
      pendingResultCallback = null;
    }
    /**
     * Returns whether this state holder accepts updates coming from the given result callback.
     *
     * <p>A null {@code resultCallback} means that the update is a regular receiver state update, in
     * which case the update will only be accepted if {@link #value} is not being masked. If {@link
     * #value} is being masked, the update will only be accepted if {@code resultCallback} is the
     * same as the {@link #pendingResultCallback}.
     *
     * @param resultCallback A result callback. May be null if the update comes from a regular
     *     receiver status update.
     */
    public boolean acceptsUpdate(@Nullable ResultCallback<?> resultCallback) {
      return pendingResultCallback == resultCallback;
    }
  }
  @RequiresApi(30)
  private final class Api30Impl {
    private final MediaRouter2 mediaRouter2;
    private final TransferCallback transferCallback;
    private final RouteCallback emptyRouteCallback;
    private final Handler handler;
    public Api30Impl(Context context) {
      mediaRouter2 = MediaRouter2.getInstance(context);
      transferCallback = new MediaRouter2TransferCallbackImpl();
      emptyRouteCallback = new MediaRouter2RouteCallbackImpl();
      handler = new Handler(Looper.getMainLooper());
    }
    /** Acquires necessary resources and registers callbacks. */
    @DoNotInline
    public void initialize() {
      mediaRouter2.registerTransferCallback(handler::post, transferCallback);
      // We need at least one route callback registered in order to get transfer callback updates.
      mediaRouter2.registerRouteCallback(
          handler::post,
          emptyRouteCallback,
          new RouteDiscoveryPreference.Builder(ImmutableList.of(), /* activeScan= */ false)
              .build());
    }
    /**
     * Releases any resources acquired in {@link #initialize()} and unregisters any registered
     * callbacks.
     */
    @DoNotInline
    public void release() {
      mediaRouter2.unregisterTransferCallback(transferCallback);
      mediaRouter2.unregisterRouteCallback(emptyRouteCallback);
      handler.removeCallbacksAndMessages(/* token= */ null);
    }
    /** Updates the device info with an up-to-date value and notifies the listeners. */
    @DoNotInline
    private void updateDeviceInfo() {
      DeviceInfo oldDeviceInfo = deviceInfo;
      DeviceInfo newDeviceInfo = fetchDeviceInfo();
      deviceInfo = newDeviceInfo;
      if (!deviceInfo.equals(oldDeviceInfo)) {
        listeners.sendEvent(
            EVENT_DEVICE_INFO_CHANGED, listener -> listener.onDeviceInfoChanged(newDeviceInfo));
      }
    }
    /**
     * Returns a {@link DeviceInfo} with the {@link RoutingController#getId() id} that corresponds
     * to the Cast session, or {@link #DEVICE_INFO_REMOTE_EMPTY} if not available.
     */
    @DoNotInline
    public DeviceInfo fetchDeviceInfo() {
      // TODO: b/364833997 - Fetch this information from the AndroidX MediaRouter selected route
      // once the selected route id matches the controller id.
      List<RoutingController> controllers = mediaRouter2.getControllers();
      // The controller at position zero is always the system controller (local playback). All other
      // controllers are for remote playback, and could be the Cast one.
      if (controllers.size() != 2) {
        // There's either no remote routing controller, or there's more than one. In either case we
        // don't populate the device info because either there's no Cast routing controller, or we
        // cannot safely identify the Cast routing controller.
        return DEVICE_INFO_REMOTE_EMPTY;
      } else {
        // There's only one remote routing controller. It's safe to assume it's the Cast routing
        // controller.
        RoutingController remoteController = controllers.get(1);
        // TODO b/364580007 - Populate volume information, and implement Player volume-related
        //  methods.
        return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE)
            .setRoutingControllerId(remoteController.getId())
            .build();
      }
    }
    /**
     * Empty {@link RouteCallback} implementation necessary for registering the {@link MediaRouter2}
     * instance with the system_server.
     *
     * <p>This callback must be registered so that the media router service notifies the {@link
     * MediaRouter2TransferCallbackImpl} of transfer events.
     */
    private final class MediaRouter2RouteCallbackImpl extends RouteCallback {}
    /**
     * {@link TransferCallback} implementation to listen for {@link RoutingController} creation and
     * releases.
     */
    private final class MediaRouter2TransferCallbackImpl extends TransferCallback {
      @Override
      public void onTransfer(RoutingController oldController, RoutingController newController) {
        updateDeviceInfo();
      }
      @Override
      public void onStop(RoutingController controller) {
        updateDeviceInfo();
      }
    }
  }
}