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();
}
}
}
}