• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.ext.cast;
17 
18 import android.os.Looper;
19 import androidx.annotation.Nullable;
20 import com.google.android.exoplayer2.BasePlayer;
21 import com.google.android.exoplayer2.C;
22 import com.google.android.exoplayer2.ExoPlaybackException;
23 import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
24 import com.google.android.exoplayer2.MediaItem;
25 import com.google.android.exoplayer2.PlaybackParameters;
26 import com.google.android.exoplayer2.Player;
27 import com.google.android.exoplayer2.Timeline;
28 import com.google.android.exoplayer2.source.TrackGroup;
29 import com.google.android.exoplayer2.source.TrackGroupArray;
30 import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
31 import com.google.android.exoplayer2.trackselection.TrackSelection;
32 import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
33 import com.google.android.exoplayer2.util.Assertions;
34 import com.google.android.exoplayer2.util.Log;
35 import com.google.android.exoplayer2.util.MimeTypes;
36 import com.google.android.gms.cast.CastStatusCodes;
37 import com.google.android.gms.cast.MediaInfo;
38 import com.google.android.gms.cast.MediaQueueItem;
39 import com.google.android.gms.cast.MediaStatus;
40 import com.google.android.gms.cast.MediaTrack;
41 import com.google.android.gms.cast.framework.CastContext;
42 import com.google.android.gms.cast.framework.CastSession;
43 import com.google.android.gms.cast.framework.SessionManager;
44 import com.google.android.gms.cast.framework.SessionManagerListener;
45 import com.google.android.gms.cast.framework.media.RemoteMediaClient;
46 import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
47 import com.google.android.gms.common.api.PendingResult;
48 import com.google.android.gms.common.api.ResultCallback;
49 import java.util.ArrayDeque;
50 import java.util.ArrayList;
51 import java.util.Iterator;
52 import java.util.List;
53 import java.util.concurrent.CopyOnWriteArrayList;
54 import org.checkerframework.checker.nullness.qual.RequiresNonNull;
55 
56 /**
57  * {@link Player} implementation that communicates with a Cast receiver app.
58  *
59  * <p>The behavior of this class depends on the underlying Cast session, which is obtained from the
60  * injected {@link CastContext}. To keep track of the session, {@link #isCastSessionAvailable()} can
61  * be queried and {@link SessionAvailabilityListener} can be implemented and attached to the player.
62  *
63  * <p>If no session is available, the player state will remain unchanged and calls to methods that
64  * alter it will be ignored. Querying the player state is possible even when no session is
65  * available, in which case, the last observed receiver app state is reported.
66  *
67  * <p>Methods should be called on the application's main thread.
68  */
69 public final class CastPlayer extends BasePlayer {
70 
71   static {
72     ExoPlayerLibraryInfo.registerModule("goog.exo.cast");
73   }
74 
75   private static final String TAG = "CastPlayer";
76 
77   private static final int RENDERER_COUNT = 3;
78   private static final int RENDERER_INDEX_VIDEO = 0;
79   private static final int RENDERER_INDEX_AUDIO = 1;
80   private static final int RENDERER_INDEX_TEXT = 2;
81   private static final long PROGRESS_REPORT_PERIOD_MS = 1000;
82   private static final TrackSelectionArray EMPTY_TRACK_SELECTION_ARRAY =
83       new TrackSelectionArray(null, null, null);
84   private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0];
85 
86   private final CastContext castContext;
87   private final MediaItemConverter mediaItemConverter;
88   // TODO: Allow custom implementations of CastTimelineTracker.
89   private final CastTimelineTracker timelineTracker;
90   private final Timeline.Period period;
91 
92   // Result callbacks.
93   private final StatusListener statusListener;
94   private final SeekResultCallback seekResultCallback;
95 
96   // Listeners and notification.
97   private final CopyOnWriteArrayList<ListenerHolder> listeners;
98   private final ArrayList<ListenerNotificationTask> notificationsBatch;
99   private final ArrayDeque<ListenerNotificationTask> ongoingNotificationsTasks;
100   @Nullable private SessionAvailabilityListener sessionAvailabilityListener;
101 
102   // Internal state.
103   private final StateHolder<Boolean> playWhenReady;
104   private final StateHolder<Integer> repeatMode;
105   @Nullable private RemoteMediaClient remoteMediaClient;
106   private CastTimeline currentTimeline;
107   private TrackGroupArray currentTrackGroups;
108   private TrackSelectionArray currentTrackSelection;
109   @Player.State private int playbackState;
110   private int currentWindowIndex;
111   private long lastReportedPositionMs;
112   private int pendingSeekCount;
113   private int pendingSeekWindowIndex;
114   private long pendingSeekPositionMs;
115 
116   /**
117    * Creates a new cast player that uses a {@link DefaultMediaItemConverter}.
118    *
119    * @param castContext The context from which the cast session is obtained.
120    */
CastPlayer(CastContext castContext)121   public CastPlayer(CastContext castContext) {
122     this(castContext, new DefaultMediaItemConverter());
123   }
124 
125   /**
126    * Creates a new cast player.
127    *
128    * @param castContext The context from which the cast session is obtained.
129    * @param mediaItemConverter The {@link MediaItemConverter} to use.
130    */
CastPlayer(CastContext castContext, MediaItemConverter mediaItemConverter)131   public CastPlayer(CastContext castContext, MediaItemConverter mediaItemConverter) {
132     this.castContext = castContext;
133     this.mediaItemConverter = mediaItemConverter;
134     timelineTracker = new CastTimelineTracker();
135     period = new Timeline.Period();
136     statusListener = new StatusListener();
137     seekResultCallback = new SeekResultCallback();
138     listeners = new CopyOnWriteArrayList<>();
139     notificationsBatch = new ArrayList<>();
140     ongoingNotificationsTasks = new ArrayDeque<>();
141 
142     playWhenReady = new StateHolder<>(false);
143     repeatMode = new StateHolder<>(REPEAT_MODE_OFF);
144     playbackState = STATE_IDLE;
145     currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
146     currentTrackGroups = TrackGroupArray.EMPTY;
147     currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
148     pendingSeekWindowIndex = C.INDEX_UNSET;
149     pendingSeekPositionMs = C.TIME_UNSET;
150 
151     SessionManager sessionManager = castContext.getSessionManager();
152     sessionManager.addSessionManagerListener(statusListener, CastSession.class);
153     CastSession session = sessionManager.getCurrentCastSession();
154     setRemoteMediaClient(session != null ? session.getRemoteMediaClient() : null);
155     updateInternalStateAndNotifyIfChanged();
156   }
157 
158   // Media Queue manipulation methods.
159 
160   /** @deprecated Use {@link #setMediaItems(List, int, long)} instead. */
161   @Deprecated
162   @Nullable
loadItem(MediaQueueItem item, long positionMs)163   public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {
164     return setMediaItemsInternal(
165         new MediaQueueItem[] {item}, /* startWindowIndex= */ 0, positionMs, repeatMode.value);
166   }
167 
168   /**
169    * @deprecated Use {@link #setMediaItems(List, int, long)} and {@link #setRepeatMode(int)}
170    *     instead.
171    */
172   @Deprecated
173   @Nullable
loadItems( MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode)174   public PendingResult<MediaChannelResult> loadItems(
175       MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
176     return setMediaItemsInternal(items, startIndex, positionMs, repeatMode);
177   }
178 
179   /** @deprecated Use {@link #addMediaItems(List)} instead. */
180   @Deprecated
181   @Nullable
addItems(MediaQueueItem... items)182   public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {
183     return addMediaItemsInternal(items, MediaQueueItem.INVALID_ITEM_ID);
184   }
185 
186   /** @deprecated Use {@link #addMediaItems(int, List)} instead. */
187   @Deprecated
188   @Nullable
addItems(int periodId, MediaQueueItem... items)189   public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {
190     if (periodId == MediaQueueItem.INVALID_ITEM_ID
191         || currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
192       return addMediaItemsInternal(items, periodId);
193     }
194     return null;
195   }
196 
197   /** @deprecated Use {@link #removeMediaItem(int)} instead. */
198   @Deprecated
199   @Nullable
removeItem(int periodId)200   public PendingResult<MediaChannelResult> removeItem(int periodId) {
201     if (currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
202       return removeMediaItemsInternal(new int[] {periodId});
203     }
204     return null;
205   }
206 
207   /** @deprecated Use {@link #moveMediaItem(int, int)} instead. */
208   @Deprecated
209   @Nullable
moveItem(int periodId, int newIndex)210   public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {
211     Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getWindowCount());
212     int fromIndex = currentTimeline.getIndexOfPeriod(periodId);
213     if (fromIndex != C.INDEX_UNSET && fromIndex != newIndex) {
214       return moveMediaItemsInternal(new int[] {periodId}, fromIndex, newIndex);
215     }
216     return null;
217   }
218 
219   /**
220    * Returns the item that corresponds to the period with the given id, or null if no media queue or
221    * period with id {@code periodId} exist.
222    *
223    * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item
224    *     to get.
225    * @return The item that corresponds to the period with the given id, or null if no media queue or
226    *     period with id {@code periodId} exist.
227    */
228   @Nullable
getItem(int periodId)229   public MediaQueueItem getItem(int periodId) {
230     MediaStatus mediaStatus = getMediaStatus();
231     return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
232         ? mediaStatus.getItemById(periodId) : null;
233   }
234 
235   // CastSession methods.
236 
237   /**
238    * Returns whether a cast session is available.
239    */
isCastSessionAvailable()240   public boolean isCastSessionAvailable() {
241     return remoteMediaClient != null;
242   }
243 
244   /**
245    * Sets a listener for updates on the cast session availability.
246    *
247    * @param listener The {@link SessionAvailabilityListener}, or null to clear the listener.
248    */
setSessionAvailabilityListener(@ullable SessionAvailabilityListener listener)249   public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) {
250     sessionAvailabilityListener = listener;
251   }
252 
253   // Player implementation.
254 
255   @Override
256   @Nullable
getAudioComponent()257   public AudioComponent getAudioComponent() {
258     return null;
259   }
260 
261   @Override
262   @Nullable
getVideoComponent()263   public VideoComponent getVideoComponent() {
264     return null;
265   }
266 
267   @Override
268   @Nullable
getTextComponent()269   public TextComponent getTextComponent() {
270     return null;
271   }
272 
273   @Override
274   @Nullable
getMetadataComponent()275   public MetadataComponent getMetadataComponent() {
276     return null;
277   }
278 
279   @Override
280   @Nullable
getDeviceComponent()281   public DeviceComponent getDeviceComponent() {
282     // TODO(b/151792305): Implement the component.
283     return null;
284   }
285 
286   @Override
getApplicationLooper()287   public Looper getApplicationLooper() {
288     return Looper.getMainLooper();
289   }
290 
291   @Override
addListener(EventListener listener)292   public void addListener(EventListener listener) {
293     listeners.addIfAbsent(new ListenerHolder(listener));
294   }
295 
296   @Override
removeListener(EventListener listener)297   public void removeListener(EventListener listener) {
298     for (ListenerHolder listenerHolder : listeners) {
299       if (listenerHolder.listener.equals(listener)) {
300         listenerHolder.release();
301         listeners.remove(listenerHolder);
302       }
303     }
304   }
305 
306   @Override
setMediaItems( List<MediaItem> mediaItems, int startWindowIndex, long startPositionMs)307   public void setMediaItems(
308       List<MediaItem> mediaItems, int startWindowIndex, long startPositionMs) {
309     setMediaItemsInternal(
310         toMediaQueueItems(mediaItems), startWindowIndex, startPositionMs, repeatMode.value);
311   }
312 
313   @Override
addMediaItems(List<MediaItem> mediaItems)314   public void addMediaItems(List<MediaItem> mediaItems) {
315     addMediaItemsInternal(toMediaQueueItems(mediaItems), MediaQueueItem.INVALID_ITEM_ID);
316   }
317 
318   @Override
addMediaItems(int index, List<MediaItem> mediaItems)319   public void addMediaItems(int index, List<MediaItem> mediaItems) {
320     Assertions.checkArgument(index >= 0);
321     int uid = MediaQueueItem.INVALID_ITEM_ID;
322     if (index < currentTimeline.getWindowCount()) {
323       uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid;
324     }
325     addMediaItemsInternal(toMediaQueueItems(mediaItems), uid);
326   }
327 
328   @Override
moveMediaItems(int fromIndex, int toIndex, int newIndex)329   public void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
330     Assertions.checkArgument(
331         fromIndex >= 0
332             && fromIndex <= toIndex
333             && toIndex <= currentTimeline.getWindowCount()
334             && newIndex >= 0
335             && newIndex < currentTimeline.getWindowCount());
336     newIndex = Math.min(newIndex, currentTimeline.getWindowCount() - (toIndex - fromIndex));
337     if (fromIndex == toIndex || fromIndex == newIndex) {
338       // Do nothing.
339       return;
340     }
341     int[] uids = new int[toIndex - fromIndex];
342     for (int i = 0; i < uids.length; i++) {
343       uids[i] = (int) currentTimeline.getWindow(/* windowIndex= */ i + fromIndex, window).uid;
344     }
345     moveMediaItemsInternal(uids, fromIndex, newIndex);
346   }
347 
348   @Override
removeMediaItems(int fromIndex, int toIndex)349   public void removeMediaItems(int fromIndex, int toIndex) {
350     Assertions.checkArgument(
351         fromIndex >= 0 && toIndex >= fromIndex && toIndex <= currentTimeline.getWindowCount());
352     if (fromIndex == toIndex) {
353       // Do nothing.
354       return;
355     }
356     int[] uids = new int[toIndex - fromIndex];
357     for (int i = 0; i < uids.length; i++) {
358       uids[i] = (int) currentTimeline.getWindow(/* windowIndex= */ i + fromIndex, window).uid;
359     }
360     removeMediaItemsInternal(uids);
361   }
362 
363   @Override
clearMediaItems()364   public void clearMediaItems() {
365     removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ currentTimeline.getWindowCount());
366   }
367 
368   @Override
prepare()369   public void prepare() {
370     // Do nothing.
371   }
372 
373   @Override
374   @Player.State
getPlaybackState()375   public int getPlaybackState() {
376     return playbackState;
377   }
378 
379   @Override
380   @PlaybackSuppressionReason
getPlaybackSuppressionReason()381   public int getPlaybackSuppressionReason() {
382     return Player.PLAYBACK_SUPPRESSION_REASON_NONE;
383   }
384 
385   @Deprecated
386   @Override
387   @Nullable
getPlaybackError()388   public ExoPlaybackException getPlaybackError() {
389     return getPlayerError();
390   }
391 
392   @Override
393   @Nullable
getPlayerError()394   public ExoPlaybackException getPlayerError() {
395     return null;
396   }
397 
398   @Override
setPlayWhenReady(boolean playWhenReady)399   public void setPlayWhenReady(boolean playWhenReady) {
400     if (remoteMediaClient == null) {
401       return;
402     }
403     // We update the local state and send the message to the receiver app, which will cause the
404     // operation to be perceived as synchronous by the user. When the operation reports a result,
405     // the local state will be updated to reflect the state reported by the Cast SDK.
406     setPlayerStateAndNotifyIfChanged(
407         playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, playbackState);
408     flushNotifications();
409     PendingResult<MediaChannelResult> pendingResult =
410         playWhenReady ? remoteMediaClient.play() : remoteMediaClient.pause();
411     this.playWhenReady.pendingResultCallback =
412         new ResultCallback<MediaChannelResult>() {
413           @Override
414           public void onResult(MediaChannelResult mediaChannelResult) {
415             if (remoteMediaClient != null) {
416               updatePlayerStateAndNotifyIfChanged(this);
417               flushNotifications();
418             }
419           }
420         };
421     pendingResult.setResultCallback(this.playWhenReady.pendingResultCallback);
422   }
423 
424   @Override
getPlayWhenReady()425   public boolean getPlayWhenReady() {
426     return playWhenReady.value;
427   }
428 
429   @Override
seekTo(int windowIndex, long positionMs)430   public void seekTo(int windowIndex, long positionMs) {
431     MediaStatus mediaStatus = getMediaStatus();
432     // We assume the default position is 0. There is no support for seeking to the default position
433     // in RemoteMediaClient.
434     positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
435     if (mediaStatus != null) {
436       if (getCurrentWindowIndex() != windowIndex) {
437         remoteMediaClient.queueJumpToItem((int) currentTimeline.getPeriod(windowIndex, period).uid,
438             positionMs, null).setResultCallback(seekResultCallback);
439       } else {
440         remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback);
441       }
442       pendingSeekCount++;
443       pendingSeekWindowIndex = windowIndex;
444       pendingSeekPositionMs = positionMs;
445       notificationsBatch.add(
446           new ListenerNotificationTask(
447               listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)));
448     } else if (pendingSeekCount == 0) {
449       notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
450     }
451     flushNotifications();
452   }
453 
454   /** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */
455   @SuppressWarnings("deprecation")
456   @Deprecated
457   @Override
setPlaybackParameters(@ullable PlaybackParameters playbackParameters)458   public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
459     // Unsupported by the RemoteMediaClient API. Do nothing.
460   }
461 
462   /** @deprecated Use {@link #getPlaybackSpeed()} instead. */
463   @SuppressWarnings("deprecation")
464   @Deprecated
465   @Override
getPlaybackParameters()466   public PlaybackParameters getPlaybackParameters() {
467     return PlaybackParameters.DEFAULT;
468   }
469 
470   @Override
setPlaybackSpeed(float playbackSpeed)471   public void setPlaybackSpeed(float playbackSpeed) {
472     // Unsupported by the RemoteMediaClient API. Do nothing.
473   }
474 
475   @Override
getPlaybackSpeed()476   public float getPlaybackSpeed() {
477     return Player.DEFAULT_PLAYBACK_SPEED;
478   }
479 
480   @Override
stop(boolean reset)481   public void stop(boolean reset) {
482     playbackState = STATE_IDLE;
483     if (remoteMediaClient != null) {
484       // TODO(b/69792021): Support or emulate stop without position reset.
485       remoteMediaClient.stop();
486     }
487   }
488 
489   @Override
release()490   public void release() {
491     SessionManager sessionManager = castContext.getSessionManager();
492     sessionManager.removeSessionManagerListener(statusListener, CastSession.class);
493     sessionManager.endCurrentSession(false);
494   }
495 
496   @Override
getRendererCount()497   public int getRendererCount() {
498     // We assume there are three renderers: video, audio, and text.
499     return RENDERER_COUNT;
500   }
501 
502   @Override
getRendererType(int index)503   public int getRendererType(int index) {
504     switch (index) {
505       case RENDERER_INDEX_VIDEO:
506         return C.TRACK_TYPE_VIDEO;
507       case RENDERER_INDEX_AUDIO:
508         return C.TRACK_TYPE_AUDIO;
509       case RENDERER_INDEX_TEXT:
510         return C.TRACK_TYPE_TEXT;
511       default:
512         throw new IndexOutOfBoundsException();
513     }
514   }
515 
516   @Override
setRepeatMode(@epeatMode int repeatMode)517   public void setRepeatMode(@RepeatMode int repeatMode) {
518     if (remoteMediaClient == null) {
519       return;
520     }
521     // We update the local state and send the message to the receiver app, which will cause the
522     // operation to be perceived as synchronous by the user. When the operation reports a result,
523     // the local state will be updated to reflect the state reported by the Cast SDK.
524     setRepeatModeAndNotifyIfChanged(repeatMode);
525     flushNotifications();
526     PendingResult<MediaChannelResult> pendingResult =
527         remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), /* jsonObject= */ null);
528     this.repeatMode.pendingResultCallback =
529         new ResultCallback<MediaChannelResult>() {
530           @Override
531           public void onResult(MediaChannelResult mediaChannelResult) {
532             if (remoteMediaClient != null) {
533               updateRepeatModeAndNotifyIfChanged(this);
534               flushNotifications();
535             }
536           }
537         };
538     pendingResult.setResultCallback(this.repeatMode.pendingResultCallback);
539   }
540 
541   @Override
getRepeatMode()542   @RepeatMode public int getRepeatMode() {
543     return repeatMode.value;
544   }
545 
546   @Override
setShuffleModeEnabled(boolean shuffleModeEnabled)547   public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
548     // TODO: Support shuffle mode.
549   }
550 
551   @Override
getShuffleModeEnabled()552   public boolean getShuffleModeEnabled() {
553     // TODO: Support shuffle mode.
554     return false;
555   }
556 
557   @Override
getCurrentTrackSelections()558   public TrackSelectionArray getCurrentTrackSelections() {
559     return currentTrackSelection;
560   }
561 
562   @Override
getCurrentTrackGroups()563   public TrackGroupArray getCurrentTrackGroups() {
564     return currentTrackGroups;
565   }
566 
567   @Override
getCurrentTimeline()568   public Timeline getCurrentTimeline() {
569     return currentTimeline;
570   }
571 
572   @Override
getCurrentPeriodIndex()573   public int getCurrentPeriodIndex() {
574     return getCurrentWindowIndex();
575   }
576 
577   @Override
getCurrentWindowIndex()578   public int getCurrentWindowIndex() {
579     return pendingSeekWindowIndex != C.INDEX_UNSET ? pendingSeekWindowIndex : currentWindowIndex;
580   }
581 
582   // TODO: Fill the cast timeline information with ProgressListener's duration updates.
583   // See [Internal: b/65152553].
584   @Override
getDuration()585   public long getDuration() {
586     return getContentDuration();
587   }
588 
589   @Override
getCurrentPosition()590   public long getCurrentPosition() {
591     return pendingSeekPositionMs != C.TIME_UNSET
592         ? pendingSeekPositionMs
593         : remoteMediaClient != null
594             ? remoteMediaClient.getApproximateStreamPosition()
595             : lastReportedPositionMs;
596   }
597 
598   @Override
getBufferedPosition()599   public long getBufferedPosition() {
600     return getCurrentPosition();
601   }
602 
603   @Override
getTotalBufferedDuration()604   public long getTotalBufferedDuration() {
605     long bufferedPosition = getBufferedPosition();
606     long currentPosition = getCurrentPosition();
607     return bufferedPosition == C.TIME_UNSET || currentPosition == C.TIME_UNSET
608         ? 0
609         : bufferedPosition - currentPosition;
610   }
611 
612   @Override
isPlayingAd()613   public boolean isPlayingAd() {
614     return false;
615   }
616 
617   @Override
getCurrentAdGroupIndex()618   public int getCurrentAdGroupIndex() {
619     return C.INDEX_UNSET;
620   }
621 
622   @Override
getCurrentAdIndexInAdGroup()623   public int getCurrentAdIndexInAdGroup() {
624     return C.INDEX_UNSET;
625   }
626 
627   @Override
isLoading()628   public boolean isLoading() {
629     return false;
630   }
631 
632   @Override
getContentPosition()633   public long getContentPosition() {
634     return getCurrentPosition();
635   }
636 
637   @Override
getContentBufferedPosition()638   public long getContentBufferedPosition() {
639     return getBufferedPosition();
640   }
641 
642   // Internal methods.
643 
updateInternalStateAndNotifyIfChanged()644   private void updateInternalStateAndNotifyIfChanged() {
645     if (remoteMediaClient == null) {
646       // There is no session. We leave the state of the player as it is now.
647       return;
648     }
649     boolean wasPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
650     updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
651     boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
652     if (wasPlaying != isPlaying) {
653       notificationsBatch.add(
654           new ListenerNotificationTask(listener -> listener.onIsPlayingChanged(isPlaying)));
655     }
656     updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
657     updateTimelineAndNotifyIfChanged();
658 
659     int currentWindowIndex = C.INDEX_UNSET;
660     MediaQueueItem currentItem = remoteMediaClient.getCurrentItem();
661     if (currentItem != null) {
662       currentWindowIndex = currentTimeline.getIndexOfPeriod(currentItem.getItemId());
663     }
664     if (currentWindowIndex == C.INDEX_UNSET) {
665       // The timeline is empty. Fall back to index 0, which is what ExoPlayer would do.
666       currentWindowIndex = 0;
667     }
668     if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
669       this.currentWindowIndex = currentWindowIndex;
670       notificationsBatch.add(
671           new ListenerNotificationTask(
672               listener ->
673                   listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION)));
674     }
675     if (updateTracksAndSelectionsAndNotifyIfChanged()) {
676       notificationsBatch.add(
677           new ListenerNotificationTask(
678               listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)));
679     }
680     flushNotifications();
681   }
682 
683   /**
684    * Updates {@link #playWhenReady} and {@link #playbackState} to match the Cast {@code
685    * remoteMediaClient} state, and notifies listeners of any state changes.
686    *
687    * <p>This method will only update values whose {@link StateHolder#pendingResultCallback} matches
688    * the given {@code resultCallback}.
689    */
690   @RequiresNonNull("remoteMediaClient")
updatePlayerStateAndNotifyIfChanged(@ullable ResultCallback<?> resultCallback)691   private void updatePlayerStateAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
692     boolean newPlayWhenReadyValue = playWhenReady.value;
693     if (playWhenReady.acceptsUpdate(resultCallback)) {
694       newPlayWhenReadyValue = !remoteMediaClient.isPaused();
695       playWhenReady.clearPendingResultCallback();
696     }
697     @PlayWhenReadyChangeReason
698     int playWhenReadyChangeReason =
699         newPlayWhenReadyValue != playWhenReady.value
700             ? PLAY_WHEN_READY_CHANGE_REASON_REMOTE
701             : PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
702     // We do not mask the playback state, so try setting it regardless of the playWhenReady masking.
703     setPlayerStateAndNotifyIfChanged(
704         newPlayWhenReadyValue, playWhenReadyChangeReason, fetchPlaybackState(remoteMediaClient));
705   }
706 
707   @RequiresNonNull("remoteMediaClient")
updateRepeatModeAndNotifyIfChanged(@ullable ResultCallback<?> resultCallback)708   private void updateRepeatModeAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
709     if (repeatMode.acceptsUpdate(resultCallback)) {
710       setRepeatModeAndNotifyIfChanged(fetchRepeatMode(remoteMediaClient));
711       repeatMode.clearPendingResultCallback();
712     }
713   }
714 
updateTimelineAndNotifyIfChanged()715   private void updateTimelineAndNotifyIfChanged() {
716     if (updateTimeline()) {
717       // TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
718       //     TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
719       notificationsBatch.add(
720           new ListenerNotificationTask(
721               listener ->
722                   listener.onTimelineChanged(
723                       currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)));
724     }
725   }
726 
727   /**
728    * Updates the current timeline and returns whether it has changed.
729    */
updateTimeline()730   private boolean updateTimeline() {
731     CastTimeline oldTimeline = currentTimeline;
732     MediaStatus status = getMediaStatus();
733     currentTimeline =
734         status != null
735             ? timelineTracker.getCastTimeline(remoteMediaClient)
736             : CastTimeline.EMPTY_CAST_TIMELINE;
737     return !oldTimeline.equals(currentTimeline);
738   }
739 
740   /** Updates the internal tracks and selection and returns whether they have changed. */
updateTracksAndSelectionsAndNotifyIfChanged()741   private boolean updateTracksAndSelectionsAndNotifyIfChanged() {
742     if (remoteMediaClient == null) {
743       // There is no session. We leave the state of the player as it is now.
744       return false;
745     }
746 
747     MediaStatus mediaStatus = getMediaStatus();
748     MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null;
749     List<MediaTrack> castMediaTracks = mediaInfo != null ? mediaInfo.getMediaTracks() : null;
750     if (castMediaTracks == null || castMediaTracks.isEmpty()) {
751       boolean hasChanged = !currentTrackGroups.isEmpty();
752       currentTrackGroups = TrackGroupArray.EMPTY;
753       currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
754       return hasChanged;
755     }
756     long[] activeTrackIds = mediaStatus.getActiveTrackIds();
757     if (activeTrackIds == null) {
758       activeTrackIds = EMPTY_TRACK_ID_ARRAY;
759     }
760 
761     TrackGroup[] trackGroups = new TrackGroup[castMediaTracks.size()];
762     TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT];
763     for (int i = 0; i < castMediaTracks.size(); i++) {
764       MediaTrack mediaTrack = castMediaTracks.get(i);
765       trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack));
766 
767       long id = mediaTrack.getId();
768       int trackType = MimeTypes.getTrackType(mediaTrack.getContentType());
769       int rendererIndex = getRendererIndexForTrackType(trackType);
770       if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET
771           && trackSelections[rendererIndex] == null) {
772         trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0);
773       }
774     }
775     TrackGroupArray newTrackGroups = new TrackGroupArray(trackGroups);
776     TrackSelectionArray newTrackSelections = new TrackSelectionArray(trackSelections);
777 
778     if (!newTrackGroups.equals(currentTrackGroups)
779         || !newTrackSelections.equals(currentTrackSelection)) {
780       currentTrackSelection = new TrackSelectionArray(trackSelections);
781       currentTrackGroups = new TrackGroupArray(trackGroups);
782       return true;
783     }
784     return false;
785   }
786 
787   @Nullable
setMediaItemsInternal( MediaQueueItem[] mediaQueueItems, int startWindowIndex, long startPositionMs, @RepeatMode int repeatMode)788   private PendingResult<MediaChannelResult> setMediaItemsInternal(
789       MediaQueueItem[] mediaQueueItems,
790       int startWindowIndex,
791       long startPositionMs,
792       @RepeatMode int repeatMode) {
793     if (remoteMediaClient == null || mediaQueueItems.length == 0) {
794       return null;
795     }
796     startPositionMs = startPositionMs == C.TIME_UNSET ? 0 : startPositionMs;
797     if (startWindowIndex == C.INDEX_UNSET) {
798       startWindowIndex = getCurrentWindowIndex();
799       startPositionMs = getCurrentPosition();
800     }
801     return remoteMediaClient.queueLoad(
802         mediaQueueItems,
803         Math.min(startWindowIndex, mediaQueueItems.length - 1),
804         getCastRepeatMode(repeatMode),
805         startPositionMs,
806         /* customData= */ null);
807   }
808 
809   @Nullable
addMediaItemsInternal(MediaQueueItem[] items, int uid)810   private PendingResult<MediaChannelResult> addMediaItemsInternal(MediaQueueItem[] items, int uid) {
811     if (remoteMediaClient == null || getMediaStatus() == null) {
812       return null;
813     }
814     return remoteMediaClient.queueInsertItems(items, uid, /* customData= */ null);
815   }
816 
817   @Nullable
moveMediaItemsInternal( int[] uids, int fromIndex, int newIndex)818   private PendingResult<MediaChannelResult> moveMediaItemsInternal(
819       int[] uids, int fromIndex, int newIndex) {
820     if (remoteMediaClient == null || getMediaStatus() == null) {
821       return null;
822     }
823     int insertBeforeIndex = fromIndex < newIndex ? newIndex + uids.length : newIndex;
824     int insertBeforeItemId = MediaQueueItem.INVALID_ITEM_ID;
825     if (insertBeforeIndex < currentTimeline.getWindowCount()) {
826       insertBeforeItemId = (int) currentTimeline.getWindow(insertBeforeIndex, window).uid;
827     }
828     return remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null);
829   }
830 
831   @Nullable
832   private PendingResult<MediaChannelResult> removeMediaItemsInternal(int[] uids) {
833     if (remoteMediaClient == null || getMediaStatus() == null) {
834       return null;
835     }
836     return remoteMediaClient.queueRemoveItems(uids, /* customData= */ null);
837   }
838 
839   private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) {
840     if (this.repeatMode.value != repeatMode) {
841       this.repeatMode.value = repeatMode;
842       notificationsBatch.add(
843           new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(repeatMode)));
844     }
845   }
846 
847   @SuppressWarnings("deprecation")
848   private void setPlayerStateAndNotifyIfChanged(
849       boolean playWhenReady,
850       @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason,
851       @Player.State int playbackState) {
852     boolean playWhenReadyChanged = this.playWhenReady.value != playWhenReady;
853     boolean playbackStateChanged = this.playbackState != playbackState;
854     if (playWhenReadyChanged || playbackStateChanged) {
855       this.playbackState = playbackState;
856       this.playWhenReady.value = playWhenReady;
857       notificationsBatch.add(
858           new ListenerNotificationTask(
859               listener -> {
860                 listener.onPlayerStateChanged(playWhenReady, playbackState);
861                 if (playbackStateChanged) {
862                   listener.onPlaybackStateChanged(playbackState);
863                 }
864                 if (playWhenReadyChanged) {
865                   listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason);
866                 }
867               }));
868     }
869   }
870 
setRemoteMediaClient(@ullable RemoteMediaClient remoteMediaClient)871   private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) {
872     if (this.remoteMediaClient == remoteMediaClient) {
873       // Do nothing.
874       return;
875     }
876     if (this.remoteMediaClient != null) {
877       this.remoteMediaClient.removeListener(statusListener);
878       this.remoteMediaClient.removeProgressListener(statusListener);
879     }
880     this.remoteMediaClient = remoteMediaClient;
881     if (remoteMediaClient != null) {
882       if (sessionAvailabilityListener != null) {
883         sessionAvailabilityListener.onCastSessionAvailable();
884       }
885       remoteMediaClient.addListener(statusListener);
886       remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
887       updateInternalStateAndNotifyIfChanged();
888     } else {
889       updateTimelineAndNotifyIfChanged();
890       if (sessionAvailabilityListener != null) {
891         sessionAvailabilityListener.onCastSessionUnavailable();
892       }
893     }
894   }
895 
896   @Nullable
getMediaStatus()897   private MediaStatus getMediaStatus() {
898     return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
899   }
900 
901   /**
902    * Retrieves the playback state from {@code remoteMediaClient} and maps it into a {@link Player}
903    * state
904    */
fetchPlaybackState(RemoteMediaClient remoteMediaClient)905   private static int fetchPlaybackState(RemoteMediaClient remoteMediaClient) {
906     int receiverAppStatus = remoteMediaClient.getPlayerState();
907     switch (receiverAppStatus) {
908       case MediaStatus.PLAYER_STATE_BUFFERING:
909         return STATE_BUFFERING;
910       case MediaStatus.PLAYER_STATE_PLAYING:
911       case MediaStatus.PLAYER_STATE_PAUSED:
912         return STATE_READY;
913       case MediaStatus.PLAYER_STATE_IDLE:
914       case MediaStatus.PLAYER_STATE_UNKNOWN:
915       default:
916         return STATE_IDLE;
917     }
918   }
919 
920   /**
921    * Retrieves the repeat mode from {@code remoteMediaClient} and maps it into a
922    * {@link Player.RepeatMode}.
923    */
924   @RepeatMode
fetchRepeatMode(RemoteMediaClient remoteMediaClient)925   private static int fetchRepeatMode(RemoteMediaClient remoteMediaClient) {
926     MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
927     if (mediaStatus == null) {
928       // No media session active, yet.
929       return REPEAT_MODE_OFF;
930     }
931     int castRepeatMode = mediaStatus.getQueueRepeatMode();
932     switch (castRepeatMode) {
933       case MediaStatus.REPEAT_MODE_REPEAT_SINGLE:
934         return REPEAT_MODE_ONE;
935       case MediaStatus.REPEAT_MODE_REPEAT_ALL:
936       case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE:
937         return REPEAT_MODE_ALL;
938       case MediaStatus.REPEAT_MODE_REPEAT_OFF:
939         return REPEAT_MODE_OFF;
940       default:
941         throw new IllegalStateException();
942     }
943   }
944 
isTrackActive(long id, long[] activeTrackIds)945   private static boolean isTrackActive(long id, long[] activeTrackIds) {
946     for (long activeTrackId : activeTrackIds) {
947       if (activeTrackId == id) {
948         return true;
949       }
950     }
951     return false;
952   }
953 
getRendererIndexForTrackType(int trackType)954   private static int getRendererIndexForTrackType(int trackType) {
955     return trackType == C.TRACK_TYPE_VIDEO
956         ? RENDERER_INDEX_VIDEO
957         : trackType == C.TRACK_TYPE_AUDIO
958             ? RENDERER_INDEX_AUDIO
959             : trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT : C.INDEX_UNSET;
960   }
961 
getCastRepeatMode(@epeatMode int repeatMode)962   private static int getCastRepeatMode(@RepeatMode int repeatMode) {
963     switch (repeatMode) {
964       case REPEAT_MODE_ONE:
965         return MediaStatus.REPEAT_MODE_REPEAT_SINGLE;
966       case REPEAT_MODE_ALL:
967         return MediaStatus.REPEAT_MODE_REPEAT_ALL;
968       case REPEAT_MODE_OFF:
969         return MediaStatus.REPEAT_MODE_REPEAT_OFF;
970       default:
971         throw new IllegalArgumentException();
972     }
973   }
974 
flushNotifications()975   private void flushNotifications() {
976     boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty();
977     ongoingNotificationsTasks.addAll(notificationsBatch);
978     notificationsBatch.clear();
979     if (recursiveNotification) {
980       // This will be handled once the current notification task is finished.
981       return;
982     }
983     while (!ongoingNotificationsTasks.isEmpty()) {
984       ongoingNotificationsTasks.peekFirst().execute();
985       ongoingNotificationsTasks.removeFirst();
986     }
987   }
988 
toMediaQueueItems(List<MediaItem> mediaItems)989   private MediaQueueItem[] toMediaQueueItems(List<MediaItem> mediaItems) {
990     MediaQueueItem[] mediaQueueItems = new MediaQueueItem[mediaItems.size()];
991     for (int i = 0; i < mediaItems.size(); i++) {
992       mediaQueueItems[i] = mediaItemConverter.toMediaQueueItem(mediaItems.get(i));
993     }
994     return mediaQueueItems;
995   }
996 
997   // Internal classes.
998 
999   private final class StatusListener
1000       implements RemoteMediaClient.Listener,
1001           SessionManagerListener<CastSession>,
1002           RemoteMediaClient.ProgressListener {
1003 
1004     // RemoteMediaClient.ProgressListener implementation.
1005 
1006     @Override
onProgressUpdated(long progressMs, long unusedDurationMs)1007     public void onProgressUpdated(long progressMs, long unusedDurationMs) {
1008       lastReportedPositionMs = progressMs;
1009     }
1010 
1011     // RemoteMediaClient.Listener implementation.
1012 
1013     @Override
onStatusUpdated()1014     public void onStatusUpdated() {
1015       updateInternalStateAndNotifyIfChanged();
1016     }
1017 
1018     @Override
onMetadataUpdated()1019     public void onMetadataUpdated() {}
1020 
1021     @Override
onQueueStatusUpdated()1022     public void onQueueStatusUpdated() {
1023       updateTimelineAndNotifyIfChanged();
1024     }
1025 
1026     @Override
onPreloadStatusUpdated()1027     public void onPreloadStatusUpdated() {}
1028 
1029     @Override
onSendingRemoteMediaRequest()1030     public void onSendingRemoteMediaRequest() {}
1031 
1032     @Override
onAdBreakStatusUpdated()1033     public void onAdBreakStatusUpdated() {}
1034 
1035     // SessionManagerListener implementation.
1036 
1037     @Override
onSessionStarted(CastSession castSession, String s)1038     public void onSessionStarted(CastSession castSession, String s) {
1039       setRemoteMediaClient(castSession.getRemoteMediaClient());
1040     }
1041 
1042     @Override
onSessionResumed(CastSession castSession, boolean b)1043     public void onSessionResumed(CastSession castSession, boolean b) {
1044       setRemoteMediaClient(castSession.getRemoteMediaClient());
1045     }
1046 
1047     @Override
onSessionEnded(CastSession castSession, int i)1048     public void onSessionEnded(CastSession castSession, int i) {
1049       setRemoteMediaClient(null);
1050     }
1051 
1052     @Override
onSessionSuspended(CastSession castSession, int i)1053     public void onSessionSuspended(CastSession castSession, int i) {
1054       setRemoteMediaClient(null);
1055     }
1056 
1057     @Override
onSessionResumeFailed(CastSession castSession, int statusCode)1058     public void onSessionResumeFailed(CastSession castSession, int statusCode) {
1059       Log.e(TAG, "Session resume failed. Error code " + statusCode + ": "
1060           + CastUtils.getLogString(statusCode));
1061     }
1062 
1063     @Override
onSessionStarting(CastSession castSession)1064     public void onSessionStarting(CastSession castSession) {
1065       // Do nothing.
1066     }
1067 
1068     @Override
onSessionStartFailed(CastSession castSession, int statusCode)1069     public void onSessionStartFailed(CastSession castSession, int statusCode) {
1070       Log.e(TAG, "Session start failed. Error code " + statusCode + ": "
1071           + CastUtils.getLogString(statusCode));
1072     }
1073 
1074     @Override
onSessionEnding(CastSession castSession)1075     public void onSessionEnding(CastSession castSession) {
1076       // Do nothing.
1077     }
1078 
1079     @Override
onSessionResuming(CastSession castSession, String s)1080     public void onSessionResuming(CastSession castSession, String s) {
1081       // Do nothing.
1082     }
1083 
1084   }
1085 
1086   private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
1087 
1088     @Override
onResult(MediaChannelResult result)1089     public void onResult(MediaChannelResult result) {
1090       int statusCode = result.getStatus().getStatusCode();
1091       if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) {
1092         Log.e(TAG, "Seek failed. Error code " + statusCode + ": "
1093             + CastUtils.getLogString(statusCode));
1094       }
1095       if (--pendingSeekCount == 0) {
1096         pendingSeekWindowIndex = C.INDEX_UNSET;
1097         pendingSeekPositionMs = C.TIME_UNSET;
1098         notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
1099         flushNotifications();
1100       }
1101     }
1102   }
1103 
1104   /** Holds the value and the masking status of a specific part of the {@link CastPlayer} state. */
1105   private static final class StateHolder<T> {
1106 
1107     /** The user-facing value of a specific part of the {@link CastPlayer} state. */
1108     public T value;
1109 
1110     /**
1111      * If {@link #value} is being masked, holds the result callback for the operation that triggered
1112      * the masking. Or null if {@link #value} is not being masked.
1113      */
1114     @Nullable public ResultCallback<MediaChannelResult> pendingResultCallback;
1115 
StateHolder(T initialValue)1116     public StateHolder(T initialValue) {
1117       value = initialValue;
1118     }
1119 
clearPendingResultCallback()1120     public void clearPendingResultCallback() {
1121       pendingResultCallback = null;
1122     }
1123 
1124     /**
1125      * Returns whether this state holder accepts updates coming from the given result callback.
1126      *
1127      * <p>A null {@code resultCallback} means that the update is a regular receiver state update, in
1128      * which case the update will only be accepted if {@link #value} is not being masked. If {@link
1129      * #value} is being masked, the update will only be accepted if {@code resultCallback} is the
1130      * same as the {@link #pendingResultCallback}.
1131      *
1132      * @param resultCallback A result callback. May be null if the update comes from a regular
1133      *     receiver status update.
1134      */
acceptsUpdate(@ullable ResultCallback<?> resultCallback)1135     public boolean acceptsUpdate(@Nullable ResultCallback<?> resultCallback) {
1136       return pendingResultCallback == resultCallback;
1137     }
1138   }
1139 
1140   private final class ListenerNotificationTask {
1141 
1142     private final Iterator<ListenerHolder> listenersSnapshot;
1143     private final ListenerInvocation listenerInvocation;
1144 
ListenerNotificationTask(ListenerInvocation listenerInvocation)1145     private ListenerNotificationTask(ListenerInvocation listenerInvocation) {
1146       this.listenersSnapshot = listeners.iterator();
1147       this.listenerInvocation = listenerInvocation;
1148     }
1149 
execute()1150     public void execute() {
1151       while (listenersSnapshot.hasNext()) {
1152         listenersSnapshot.next().invoke(listenerInvocation);
1153       }
1154     }
1155   }
1156 }
1157