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