diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 87d5792c49c..10270dae2bb 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -650,6 +650,7 @@ private void updateInternalStateAndNotifyIfChanged() { // There is no session. We leave the state of the player as it is now. return; } + int previousWindowIndex = this.currentWindowIndex; boolean wasPlaying = playbackState == Player.STATE_READY && playWhenReady.value; updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null); boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady.value; @@ -658,10 +659,12 @@ private void updateInternalStateAndNotifyIfChanged() { Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying)); } updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null); - updateTimelineAndNotifyIfChanged(); + boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged(); int currentWindowIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline); - if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) { + if (!playingPeriodChangedByTimelineChange + && previousWindowIndex != currentWindowIndex + && pendingSeekCount == 0) { this.currentWindowIndex = currentWindowIndex; // TODO(b/181262841): call new onPositionDiscontinuity callback listeners.queueEvent( @@ -714,10 +717,16 @@ private void updateRepeatModeAndNotifyIfChanged(@Nullable ResultCallback resu } } + /** + * Updates the timeline and notifies {@link Player.EventListener event listeners} if required. + * + * @return Whether the timeline change has caused a change of the period currently being played. + */ @SuppressWarnings("deprecation") // Calling deprecated listener method. - private void updateTimelineAndNotifyIfChanged() { + private boolean updateTimelineAndNotifyIfChanged() { Timeline previousTimeline = currentTimeline; int previousWindowIndex = currentWindowIndex; + boolean playingPeriodChanged = false; if (updateTimeline()) { // TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and // TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553]. @@ -732,17 +741,14 @@ private void updateTimelineAndNotifyIfChanged() { updateAvailableCommandsAndNotifyIfChanged(); - boolean mediaItemTransitioned; - if (currentTimeline.isEmpty() && previousTimeline.isEmpty()) { - mediaItemTransitioned = false; - } else if (currentTimeline.isEmpty() != previousTimeline.isEmpty()) { - mediaItemTransitioned = true; - } else { + if (currentTimeline.isEmpty() != previousTimeline.isEmpty()) { + // Timeline initially populated or timeline cleared. + playingPeriodChanged = true; + } else if (!currentTimeline.isEmpty()) { Object previousWindowUid = previousTimeline.getWindow(previousWindowIndex, window).uid; - Object currentWindowUid = currentTimeline.getWindow(currentWindowIndex, window).uid; - mediaItemTransitioned = !currentWindowUid.equals(previousWindowUid); + playingPeriodChanged = currentTimeline.getIndexOfPeriod(previousWindowUid) == C.INDEX_UNSET; } - if (mediaItemTransitioned) { + if (playingPeriodChanged) { listeners.queueEvent( Player.EVENT_MEDIA_ITEM_TRANSITION, listener -> @@ -750,6 +756,7 @@ private void updateTimelineAndNotifyIfChanged() { getCurrentMediaItem(), MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); } } + return playingPeriodChanged; } /** diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java index 49e7bd38e9a..2309f8fd748 100644 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java @@ -76,7 +76,9 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; +import org.mockito.InOrder; import org.mockito.Mock; +import org.mockito.Mockito; /** Tests for {@link CastPlayer}. */ @RunWith(AndroidJUnit4.class) @@ -592,6 +594,46 @@ public void seekTo_sameWindow_doesNotNotifyMediaItemTransition() { verify(mockListener).onMediaItemTransition(any(), anyInt()); } + @Test + @SuppressWarnings("deprecation") // Mocks deprecated method used by the CastPlayer. + public void autoTransition_notifiesMediaItemTransitionAndPositionDiscontinuity() { + int[] mediaQueueItemIds = new int[] {1, 2}; + int[] streamTypes = {MediaInfo.STREAM_TYPE_BUFFERED, MediaInfo.STREAM_TYPE_BUFFERED}; + long[] durationsFirstMs = {12500, C.TIME_UNSET}; + // When the remote Cast player transitions to an item that wasn't played before, the media state + // delivers the duration for that media item which updates the timeline accordingly. + long[] durationsSecondMs = {12500, 22000}; + List mediaItems = createMediaItems(mediaQueueItemIds); + + castPlayer.addMediaItems(mediaItems); + updateTimeLine( + mediaItems, + mediaQueueItemIds, + /* currentItemId= */ 1, + /* streamTypes= */ streamTypes, + /* durationsMs= */ durationsFirstMs); + updateTimeLine( + mediaItems, + mediaQueueItemIds, + /* currentItemId= */ 2, + /* streamTypes= */ streamTypes, + /* durationsMs= */ durationsSecondMs); + + InOrder inOrder = Mockito.inOrder(mockListener); + inOrder + .verify(mockListener) + .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + inOrder + .verify(mockListener) + .onPositionDiscontinuity(eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION)); + inOrder + .verify(mockListener) + .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO)); + inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); + inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt()); + inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt()); + } + @Test public void isCommandAvailable_isTrueForAvailableCommands() { int[] mediaQueueItemIds = new int[] {1, 2}; @@ -1038,7 +1080,7 @@ private void updateTimeLine( .thenReturn(currentItemId == C.INDEX_UNSET ? 0 : currentItemId); // Call listener to update the timeline of the player. - remoteMediaClientCallback.onQueueStatusUpdated(); + remoteMediaClientCallback.onStatusUpdated(); } private static Player.Commands createWithPermanentCommands(