diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ed05a841da..cc4841b3fb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -35,6 +35,11 @@ * Leanback extension: * Listen to `playWhenReady` changes in `LeanbackAdapter` ([10420](https://github.com/google/ExoPlayer/issues/10420)). +* Cast: + * Use the `MediaItem` that has been passed to the playlist methods as + `Window.mediaItem` in `CastTimeline` + ([#25](https://github.com/androidx/media/issues/25), + [#8212](https://github.com/google/ExoPlayer/issues/8212)). ### 1.0.0-beta01 (2022-06-16) diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java index 4ec40f6846..d55d73e7bd 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java @@ -200,7 +200,7 @@ public CastPlayer( this.mediaItemConverter = mediaItemConverter; this.seekBackIncrementMs = seekBackIncrementMs; this.seekForwardIncrementMs = seekForwardIncrementMs; - timelineTracker = new CastTimelineTracker(); + timelineTracker = new CastTimelineTracker(mediaItemConverter); period = new Timeline.Period(); statusListener = new StatusListener(); seekResultCallback = new SeekResultCallback(); @@ -285,8 +285,7 @@ public void setMediaItems(List mediaItems, boolean resetPosition) { @Override public void setMediaItems(List mediaItems, int startIndex, long startPositionMs) { - setMediaItemsInternal( - toMediaQueueItems(mediaItems), startIndex, startPositionMs, repeatMode.value); + setMediaItemsInternal(mediaItems, startIndex, startPositionMs, repeatMode.value); } @Override @@ -296,7 +295,7 @@ public void addMediaItems(int index, List mediaItems) { if (index < currentTimeline.getWindowCount()) { uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid; } - addMediaItemsInternal(toMediaQueueItems(mediaItems), uid); + addMediaItemsInternal(mediaItems, uid); } @Override @@ -1022,14 +1021,13 @@ private void updateAvailableCommandsAndNotifyIfChanged() { } } - @Nullable - private PendingResult setMediaItemsInternal( - MediaQueueItem[] mediaQueueItems, + private void setMediaItemsInternal( + List mediaItems, int startIndex, long startPositionMs, @RepeatMode int repeatMode) { - if (remoteMediaClient == null || mediaQueueItems.length == 0) { - return null; + if (remoteMediaClient == null || mediaItems.isEmpty()) { + return; } startPositionMs = startPositionMs == C.TIME_UNSET ? 0 : startPositionMs; if (startIndex == C.INDEX_UNSET) { @@ -1040,34 +1038,35 @@ private PendingResult setMediaItemsInternal( if (!currentTimeline.isEmpty()) { pendingMediaItemRemovalPosition = getCurrentPositionInfo(); } - return remoteMediaClient.queueLoad( + MediaQueueItem[] mediaQueueItems = toMediaQueueItems(mediaItems); + timelineTracker.onMediaItemsSet(mediaItems, mediaQueueItems); + remoteMediaClient.queueLoad( mediaQueueItems, - min(startIndex, mediaQueueItems.length - 1), + min(startIndex, mediaItems.size() - 1), getCastRepeatMode(repeatMode), startPositionMs, /* customData= */ null); } - @Nullable - private PendingResult addMediaItemsInternal(MediaQueueItem[] items, int uid) { + private void addMediaItemsInternal(List mediaItems, int uid) { if (remoteMediaClient == null || getMediaStatus() == null) { - return null; + return; } - return remoteMediaClient.queueInsertItems(items, uid, /* customData= */ null); + MediaQueueItem[] itemsToInsert = toMediaQueueItems(mediaItems); + timelineTracker.onMediaItemsAdded(mediaItems, itemsToInsert); + remoteMediaClient.queueInsertItems(itemsToInsert, uid, /* customData= */ null); } - @Nullable - private PendingResult moveMediaItemsInternal( - int[] uids, int fromIndex, int newIndex) { + private void moveMediaItemsInternal(int[] uids, int fromIndex, int newIndex) { if (remoteMediaClient == null || getMediaStatus() == null) { - return null; + return; } int insertBeforeIndex = fromIndex < newIndex ? newIndex + uids.length : newIndex; int insertBeforeItemId = MediaQueueItem.INVALID_ITEM_ID; if (insertBeforeIndex < currentTimeline.getWindowCount()) { insertBeforeItemId = (int) currentTimeline.getWindow(insertBeforeIndex, window).uid; } - return remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null); + remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null); } @Nullable diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastTimeline.java b/libraries/cast/src/main/java/androidx/media3/cast/CastTimeline.java index 12e8ee5d2d..d21fca2608 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastTimeline.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastTimeline.java @@ -15,13 +15,13 @@ */ package androidx.media3.cast; -import android.net.Uri; import android.util.SparseArray; import android.util.SparseIntArray; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.Timeline; +import com.google.android.gms.cast.MediaInfo; import java.util.Arrays; /** A {@link Timeline} for Cast media queues. */ @@ -30,12 +30,16 @@ /** Holds {@link Timeline} related data for a Cast media item. */ public static final class ItemData { + /* package */ static final String UNKNOWN_CONTENT_ID = "UNKNOWN_CONTENT_ID"; + /** Holds no media information. */ public static final ItemData EMPTY = new ItemData( /* durationUs= */ C.TIME_UNSET, /* defaultPositionUs= */ C.TIME_UNSET, - /* isLive= */ false); + /* isLive= */ false, + MediaItem.EMPTY, + UNKNOWN_CONTENT_ID); /** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */ public final long durationUs; @@ -45,6 +49,10 @@ public static final class ItemData { public final long defaultPositionUs; /** Whether the item is live content, or {@code false} if unknown. */ public final boolean isLive; + /** The original media item that has been set or added to the playlist. */ + public final MediaItem mediaItem; + /** The {@linkplain MediaInfo#getContentId() content ID} of the cast media queue item. */ + public final String contentId; /** * Creates an instance. @@ -52,11 +60,20 @@ public static final class ItemData { * @param durationUs See {@link #durationsUs}. * @param defaultPositionUs See {@link #defaultPositionUs}. * @param isLive See {@link #isLive}. + * @param mediaItem See {@link #mediaItem}. + * @param contentId See {@link #contentId}. */ - public ItemData(long durationUs, long defaultPositionUs, boolean isLive) { + public ItemData( + long durationUs, + long defaultPositionUs, + boolean isLive, + MediaItem mediaItem, + String contentId) { this.durationUs = durationUs; this.defaultPositionUs = defaultPositionUs; this.isLive = isLive; + this.mediaItem = mediaItem; + this.contentId = contentId; } /** @@ -66,14 +83,23 @@ public ItemData(long durationUs, long defaultPositionUs, boolean isLive) { * @param defaultPositionUs The default start position in microseconds, or {@link C#TIME_UNSET} * if unknown. * @param isLive Whether the item is live, or {@code false} if unknown. + * @param mediaItem The media item. + * @param contentId The content ID. */ - public ItemData copyWithNewValues(long durationUs, long defaultPositionUs, boolean isLive) { + public ItemData copyWithNewValues( + long durationUs, + long defaultPositionUs, + boolean isLive, + MediaItem mediaItem, + String contentId) { if (durationUs == this.durationUs && defaultPositionUs == this.defaultPositionUs - && isLive == this.isLive) { + && isLive == this.isLive + && contentId.equals(this.contentId) + && mediaItem.equals(this.mediaItem)) { return this; } - return new ItemData(durationUs, defaultPositionUs, isLive); + return new ItemData(durationUs, defaultPositionUs, isLive, mediaItem, contentId); } } @@ -82,6 +108,7 @@ public ItemData copyWithNewValues(long durationUs, long defaultPositionUs, boole new CastTimeline(new int[0], new SparseArray<>()); private final SparseIntArray idsToIndex; + private final MediaItem[] mediaItems; private final int[] ids; private final long[] durationsUs; private final long[] defaultPositionsUs; @@ -100,10 +127,12 @@ public CastTimeline(int[] itemIds, SparseArray itemIdToData) { durationsUs = new long[itemCount]; defaultPositionsUs = new long[itemCount]; isLive = new boolean[itemCount]; + mediaItems = new MediaItem[itemCount]; for (int i = 0; i < ids.length; i++) { int id = ids[i]; idsToIndex.put(id, i); ItemData data = itemIdToData.get(id, ItemData.EMPTY); + mediaItems[i] = data.mediaItem.buildUpon().setTag(id).build(); durationsUs[i] = data.durationUs; defaultPositionsUs[i] = data.defaultPositionUs == C.TIME_UNSET ? 0 : data.defaultPositionUs; isLive[i] = data.isLive; @@ -121,18 +150,16 @@ public int getWindowCount() { public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { long durationUs = durationsUs[windowIndex]; boolean isDynamic = durationUs == C.TIME_UNSET; - MediaItem mediaItem = - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build(); return window.set( /* uid= */ ids[windowIndex], - /* mediaItem= */ mediaItem, + /* mediaItem= */ mediaItems[windowIndex], /* manifest= */ null, /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* isSeekable= */ !isDynamic, isDynamic, - isLive[windowIndex] ? mediaItem.liveConfiguration : null, + isLive[windowIndex] ? mediaItems[windowIndex].liveConfiguration : null, defaultPositionsUs[windowIndex], durationUs, /* firstPeriodIndex= */ windowIndex, diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastTimelineTracker.java b/libraries/cast/src/main/java/androidx/media3/cast/CastTimelineTracker.java index e123495152..c955387ff4 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastTimelineTracker.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastTimelineTracker.java @@ -15,14 +15,23 @@ */ package androidx.media3.cast; +import static androidx.media3.cast.CastTimeline.ItemData.UNKNOWN_CONTENT_ID; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkStateNotNull; + import android.util.SparseArray; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.media3.common.C; +import androidx.media3.common.MediaItem; +import androidx.media3.common.Player; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.framework.media.RemoteMediaClient; +import java.util.HashMap; import java.util.HashSet; +import java.util.List; /** * Creates {@link CastTimeline CastTimelines} from cast receiver app status updates. @@ -33,9 +42,47 @@ /* package */ final class CastTimelineTracker { private final SparseArray itemIdToData; + private final MediaItemConverter mediaItemConverter; + @VisibleForTesting /* package */ final HashMap mediaItemsByContentId; - public CastTimelineTracker() { + /** + * Creates an instance. + * + * @param mediaItemConverter The converter used to convert from a {@link MediaQueueItem} to a + * {@link MediaItem}. + */ + public CastTimelineTracker(MediaItemConverter mediaItemConverter) { + this.mediaItemConverter = mediaItemConverter; itemIdToData = new SparseArray<>(); + mediaItemsByContentId = new HashMap<>(); + } + + /** + * Called when media items {@linkplain Player#setMediaItems have been set to the playlist} and are + * sent to the cast playback queue. A future queue update of the {@link RemoteMediaClient} will + * reflect this addition. + * + * @param mediaItems The media items that have been set. + * @param mediaQueueItems The corresponding media queue items. + */ + public void onMediaItemsSet(List mediaItems, MediaQueueItem[] mediaQueueItems) { + mediaItemsByContentId.clear(); + onMediaItemsAdded(mediaItems, mediaQueueItems); + } + + /** + * Called when media items {@linkplain Player#addMediaItems(List) have been added} and are sent to + * the cast playback queue. A future queue update of the {@link RemoteMediaClient} will reflect + * this addition. + * + * @param mediaItems The media items that have been added. + * @param mediaQueueItems The corresponding media queue items. + */ + public void onMediaItemsAdded(List mediaItems, MediaQueueItem[] mediaQueueItems) { + for (int i = 0; i < mediaItems.size(); i++) { + mediaItemsByContentId.put( + checkNotNull(mediaQueueItems[i].getMedia()).getContentId(), mediaItems.get(i)); + } } /** @@ -63,18 +110,36 @@ public CastTimeline getCastTimeline(RemoteMediaClient remoteMediaClient) { } int currentItemId = mediaStatus.getCurrentItemId(); + String currentContentId = checkStateNotNull(mediaStatus.getMediaInfo()).getContentId(); + MediaItem mediaItem = mediaItemsByContentId.get(currentContentId); updateItemData( - currentItemId, mediaStatus.getMediaInfo(), /* defaultPositionUs= */ C.TIME_UNSET); + currentItemId, + mediaItem != null ? mediaItem : MediaItem.EMPTY, + mediaStatus.getMediaInfo(), + currentContentId, + /* defaultPositionUs= */ C.TIME_UNSET); - for (MediaQueueItem item : mediaStatus.getQueueItems()) { - long defaultPositionUs = (long) (item.getStartTime() * C.MICROS_PER_SECOND); - updateItemData(item.getItemId(), item.getMedia(), defaultPositionUs); + for (MediaQueueItem queueItem : mediaStatus.getQueueItems()) { + long defaultPositionUs = (long) (queueItem.getStartTime() * C.MICROS_PER_SECOND); + @Nullable MediaInfo mediaInfo = queueItem.getMedia(); + String contentId = mediaInfo != null ? mediaInfo.getContentId() : UNKNOWN_CONTENT_ID; + mediaItem = mediaItemsByContentId.get(contentId); + updateItemData( + queueItem.getItemId(), + mediaItem != null ? mediaItem : mediaItemConverter.toMediaItem(queueItem), + mediaInfo, + contentId, + defaultPositionUs); } - return new CastTimeline(itemIds, itemIdToData); } - private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defaultPositionUs) { + private void updateItemData( + int itemId, + MediaItem mediaItem, + @Nullable MediaInfo mediaInfo, + String contentId, + long defaultPositionUs) { CastTimeline.ItemData previousData = itemIdToData.get(itemId, CastTimeline.ItemData.EMPTY); long durationUs = CastUtils.getStreamDurationUs(mediaInfo); if (durationUs == C.TIME_UNSET) { @@ -87,7 +152,10 @@ private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defa if (defaultPositionUs == C.TIME_UNSET) { defaultPositionUs = previousData.defaultPositionUs; } - itemIdToData.put(itemId, previousData.copyWithNewValues(durationUs, defaultPositionUs, isLive)); + itemIdToData.put( + itemId, + previousData.copyWithNewValues( + durationUs, defaultPositionUs, isLive, mediaItem, contentId)); } private void removeUnusedItemDataEntries(int[] itemIds) { @@ -99,6 +167,8 @@ private void removeUnusedItemDataEntries(int[] itemIds) { int index = 0; while (index < itemIdToData.size()) { if (!scratchItemIds.contains(itemIdToData.keyAt(index))) { + CastTimeline.ItemData itemData = itemIdToData.valueAt(index); + mediaItemsByContentId.remove(itemData.contentId); itemIdToData.removeAt(index); } else { index++; diff --git a/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java b/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java index 31b7afd87a..83273d2a9a 100644 --- a/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java +++ b/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java @@ -63,6 +63,7 @@ import android.net.Uri; import androidx.media3.common.C; import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; import androidx.media3.common.MimeTypes; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; @@ -126,6 +127,7 @@ public void setUp() { when(mockCastSession.getRemoteMediaClient()).thenReturn(mockRemoteMediaClient); when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus); when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue); + when(mockMediaStatus.getMediaInfo()).thenReturn(new MediaInfo.Builder("contentId").build()); when(mockMediaQueue.getItemIds()).thenReturn(new int[0]); // Make the remote media client present the same default values as ExoPlayer: when(mockRemoteMediaClient.isPaused()).thenReturn(true); @@ -388,7 +390,7 @@ public void setMediaItems_callsRemoteMediaClient() { mediaItems.add( new MediaItem.Builder().setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build()); - castPlayer.setMediaItems(mediaItems, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L); + castPlayer.setMediaItems(mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 2000L); verify(mockRemoteMediaClient) .queueLoad(queueItemsArgumentCaptor.capture(), eq(1), anyInt(), eq(2000L), any()); @@ -424,32 +426,42 @@ public void setMediaItems_replaceExistingPlaylist_notifiesMediaItemTransition() String uri1 = "http://www.google.com/video1"; String uri2 = "http://www.google.com/video2"; firstPlaylist.add( - new MediaItem.Builder().setUri(uri1).setMimeType(MimeTypes.APPLICATION_MPD).build()); + new MediaItem.Builder() + .setUri(uri1) + .setMimeType(MimeTypes.APPLICATION_MPD) + .setTag(1) + .build()); firstPlaylist.add( - new MediaItem.Builder().setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build()); + new MediaItem.Builder() + .setUri(uri2) + .setMimeType(MimeTypes.APPLICATION_MP4) + .setTag(2) + .build()); ImmutableList secondPlaylist = ImmutableList.of( new MediaItem.Builder() .setUri(Uri.EMPTY) + .setTag(3) .setMimeType(MimeTypes.APPLICATION_MPD) .build()); - castPlayer.setMediaItems( - firstPlaylist, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L); + castPlayer.setMediaItems(firstPlaylist, /* startIndex= */ 1, /* startPositionMs= */ 2000L); updateTimeLine( firstPlaylist, /* mediaQueueItemIds= */ new int[] {1, 2}, /* currentItemId= */ 2); // Replacing existing playlist. - castPlayer.setMediaItems( - secondPlaylist, /* startWindowIndex= */ 0, /* startPositionMs= */ 1000L); + castPlayer.setMediaItems(secondPlaylist, /* startIndex= */ 0, /* startPositionMs= */ 1000L); updateTimeLine(secondPlaylist, /* mediaQueueItemIds= */ new int[] {3}, /* currentItemId= */ 3); InOrder inOrder = Mockito.inOrder(mockListener); inOrder - .verify(mockListener, times(2)) + .verify(mockListener) .onMediaItemTransition( - mediaItemCaptor.capture(), eq(MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + eq(firstPlaylist.get(1)), eq(MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + inOrder + .verify(mockListener) + .onMediaItemTransition( + eq(secondPlaylist.get(0)), eq(MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); - assertThat(mediaItemCaptor.getAllValues().get(1).localConfiguration.tag).isEqualTo(3); } @SuppressWarnings("deprecation") // Verifies deprecated callback being called correctly. @@ -459,18 +471,26 @@ public void setMediaItems_replaceExistingPlaylist_notifiesPositionDiscontinuity( String uri1 = "http://www.google.com/video1"; String uri2 = "http://www.google.com/video2"; firstPlaylist.add( - new MediaItem.Builder().setUri(uri1).setMimeType(MimeTypes.APPLICATION_MPD).build()); + new MediaItem.Builder() + .setUri(uri1) + .setMimeType(MimeTypes.APPLICATION_MPD) + .setTag(1) + .build()); firstPlaylist.add( - new MediaItem.Builder().setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build()); + new MediaItem.Builder() + .setUri(uri2) + .setMimeType(MimeTypes.APPLICATION_MP4) + .setTag(2) + .build()); ImmutableList secondPlaylist = ImmutableList.of( new MediaItem.Builder() .setUri(Uri.EMPTY) .setMimeType(MimeTypes.APPLICATION_MPD) + .setTag(3) .build()); - castPlayer.setMediaItems( - firstPlaylist, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L); + castPlayer.setMediaItems(firstPlaylist, /* startIndex= */ 1, /* startPositionMs= */ 2000L); updateTimeLine( firstPlaylist, /* mediaQueueItemIds= */ new int[] {1, 2}, @@ -481,8 +501,7 @@ public void setMediaItems_replaceExistingPlaylist_notifiesPositionDiscontinuity( /* durationsMs= */ new long[] {20_000, 20_000}, /* positionMs= */ 2000L); // Replacing existing playlist. - castPlayer.setMediaItems( - secondPlaylist, /* startWindowIndex= */ 0, /* startPositionMs= */ 1000L); + castPlayer.setMediaItems(secondPlaylist, /* startIndex= */ 0, /* startPositionMs= */ 1000L); updateTimeLine( secondPlaylist, /* mediaQueueItemIds= */ new int[] {3}, @@ -494,8 +513,8 @@ public void setMediaItems_replaceExistingPlaylist_notifiesPositionDiscontinuity( Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 2, - /* windowIndex= */ 1, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(), + /* mediaItemIndex= */ 1, + firstPlaylist.get(1), /* periodUid= */ 2, /* periodIndex= */ 1, /* positionMs= */ 2000, @@ -505,8 +524,8 @@ public void setMediaItems_replaceExistingPlaylist_notifiesPositionDiscontinuity( Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 3, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(3).build(), + /* mediaItemIndex= */ 0, + secondPlaylist.get(0), /* periodUid= */ 3, /* periodIndex= */ 0, /* positionMs= */ 1000, @@ -720,10 +739,8 @@ public void addMediaItems_notifiesMediaItemTransition() { inOrder .verify(mockListener) .onMediaItemTransition( - mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + eq(mediaItem), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); - assertThat(mediaItemCaptor.getValue().localConfiguration.tag) - .isEqualTo(mediaItem.localConfiguration.tag); } @Test @@ -742,7 +759,8 @@ public void clearMediaItems_notifiesMediaItemTransition() { InOrder inOrder = Mockito.inOrder(mockListener); inOrder .verify(mockListener) - .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + .onMediaItemTransition( + eq(mediaItems.get(0)), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder .verify(mockListener) .onMediaItemTransition( @@ -776,8 +794,8 @@ public void clearMediaItems_notifiesPositionDiscontinuity() { Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(), + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 1234, @@ -787,7 +805,7 @@ public void clearMediaItems_notifiesPositionDiscontinuity() { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ null, - /* windowIndex= */ 0, + /* mediaItemIndex= */ 0, /* mediaItem= */ null, /* periodUid= */ null, /* periodIndex= */ 0, @@ -827,10 +845,8 @@ public void removeCurrentMediaItem_notifiesMediaItemTransition() { .onMediaItemTransition( mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); - assertThat(mediaItemCaptor.getAllValues().get(0).localConfiguration.tag) - .isEqualTo(mediaItem1.localConfiguration.tag); - assertThat(mediaItemCaptor.getAllValues().get(1).localConfiguration.tag) - .isEqualTo(mediaItem2.localConfiguration.tag); + assertThat(mediaItemCaptor.getAllValues().get(0)).isEqualTo(mediaItem1); + assertThat(mediaItemCaptor.getAllValues().get(1)).isEqualTo(mediaItem2); } @Test @@ -862,8 +878,8 @@ public void removeCurrentMediaItem_notifiesPositionDiscontinuity() { Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(), + /* mediaItemIndex= */ 0, + mediaItem1, /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 1234, @@ -873,8 +889,8 @@ public void removeCurrentMediaItem_notifiesPositionDiscontinuity() { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 2, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(), + /* mediaItemIndex= */ 0, + mediaItem2, /* periodUid= */ 2, /* periodIndex= */ 0, /* positionMs= */ 0, @@ -912,10 +928,8 @@ public void removeCurrentMediaItem_byRemoteClient_notifiesMediaItemTransition() mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); List capturedMediaItems = mediaItemCaptor.getAllValues(); - assertThat(capturedMediaItems.get(0).localConfiguration.tag) - .isEqualTo(mediaItem1.localConfiguration.tag); - assertThat(capturedMediaItems.get(1).localConfiguration.tag) - .isEqualTo(mediaItem2.localConfiguration.tag); + assertThat(capturedMediaItems.get(0)).isEqualTo(mediaItem1); + assertThat(capturedMediaItems.get(1)).isEqualTo(mediaItem2); } @Test @@ -945,8 +959,8 @@ public void removeCurrentMediaItem_byRemoteClient_notifiesPositionDiscontinuity( Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(), + /* mediaItemIndex= */ 0, + mediaItem1, /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 0, // position at which we receive the timeline change @@ -956,8 +970,8 @@ public void removeCurrentMediaItem_byRemoteClient_notifiesPositionDiscontinuity( Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 2, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(), + /* mediaItemIndex= */ 0, + mediaItem2, /* periodUid= */ 2, /* periodIndex= */ 0, /* positionMs= */ 0, @@ -992,7 +1006,8 @@ public void removeNonCurrentMediaItem_doesNotNotifyMediaItemTransition() { InOrder inOrder = Mockito.inOrder(mockListener); inOrder .verify(mockListener) - .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + .onMediaItemTransition( + eq(mediaItem1), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); } @@ -1027,19 +1042,17 @@ public void seekTo_otherWindow_notifiesMediaItemTransition() { castPlayer.addMediaItems(mediaItems); updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); - castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1234); + castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 1234); InOrder inOrder = Mockito.inOrder(mockListener); inOrder .verify(mockListener) - .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + .onMediaItemTransition( + eq(mediaItem1), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder .verify(mockListener) - .onMediaItemTransition( - mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK)); + .onMediaItemTransition(eq(mediaItem2), eq(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK)); inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt()); - assertThat(mediaItemCaptor.getValue().localConfiguration.tag) - .isEqualTo(mediaItem2.localConfiguration.tag); } @Test @@ -1054,13 +1067,13 @@ public void seekTo_otherWindow_notifiesPositionDiscontinuity() { castPlayer.addMediaItems(mediaItems); updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); - castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1234); + castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 1234); Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(), + /* mediaItemIndex= */ 0, + mediaItem1, /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 0, @@ -1070,8 +1083,8 @@ public void seekTo_otherWindow_notifiesPositionDiscontinuity() { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 2, - /* windowIndex= */ 1, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(), + /* mediaItemIndex= */ 1, + mediaItem2, /* periodUid= */ 2, /* periodIndex= */ 1, /* positionMs= */ 1234, @@ -1097,12 +1110,13 @@ public void seekTo_sameWindow_doesNotNotifyMediaItemTransition() { castPlayer.addMediaItems(mediaItems); updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); - castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1234); + castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 1234); InOrder inOrder = Mockito.inOrder(mockListener); inOrder .verify(mockListener) - .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + .onMediaItemTransition( + eq(mediaItems.get(0)), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); } @@ -1115,14 +1129,13 @@ public void seekTo_sameWindow_notifiesPositionDiscontinuity() { castPlayer.addMediaItems(mediaItems); updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); - castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1234); + castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 1234); - MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(); Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - mediaItem, + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 0, @@ -1132,8 +1145,8 @@ public void seekTo_sameWindow_notifiesPositionDiscontinuity() { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - mediaItem, + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 1234, @@ -1164,13 +1177,12 @@ public void autoTransition_notifiesMediaItemTransition() { InOrder inOrder = Mockito.inOrder(mockListener); inOrder .verify(mockListener) - .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + .onMediaItemTransition( + eq(mediaItems.get(0)), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder .verify(mockListener) - .onMediaItemTransition( - mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO)); + .onMediaItemTransition(eq(mediaItems.get(1)), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO)); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); - assertThat(mediaItemCaptor.getValue().localConfiguration.tag).isEqualTo(2); } @Test @@ -1203,8 +1215,8 @@ public void autoTransition_notifiesPositionDiscontinuity() { Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(), + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 12500, @@ -1214,8 +1226,8 @@ public void autoTransition_notifiesPositionDiscontinuity() { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 2, - /* windowIndex= */ 1, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(), + /* mediaItemIndex= */ 1, + mediaItems.get(1), /* periodUid= */ 2, /* periodIndex= */ 1, /* positionMs= */ 0, @@ -1250,12 +1262,11 @@ public void seekBack_notifiesPositionDiscontinuity() { mediaItems, mediaQueueItemIds, currentItemId, streamTypes, durationsMs, positionMs); castPlayer.seekBack(); - MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(); Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - mediaItem, + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 2 * C.DEFAULT_SEEK_BACK_INCREMENT_MS, @@ -1265,8 +1276,8 @@ public void seekBack_notifiesPositionDiscontinuity() { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - mediaItem, + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ C.DEFAULT_SEEK_BACK_INCREMENT_MS, @@ -1299,12 +1310,11 @@ public void seekForward_notifiesPositionDiscontinuity() { mediaItems, mediaQueueItemIds, currentItemId, streamTypes, durationsMs, positionMs); castPlayer.seekForward(); - MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(); Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - mediaItem, + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 0, @@ -1314,8 +1324,8 @@ public void seekForward_notifiesPositionDiscontinuity() { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - mediaItem, + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ C.DEFAULT_SEEK_FORWARD_INCREMENT_MS, @@ -1475,14 +1485,14 @@ public void seekTo_nextWindow_notifiesAvailableCommandsChanged() { // Check that there were no other calls to onAvailableCommandsChanged. verify(mockListener).onAvailableCommandsChanged(any()); - castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0); + castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 0); verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousAndNextWindow); verify(mockListener, times(2)).onAvailableCommandsChanged(any()); - castPlayer.seekTo(/* windowIndex= */ 2, /* positionMs= */ 0); + castPlayer.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ 0); verify(mockListener, times(2)).onAvailableCommandsChanged(any()); - castPlayer.seekTo(/* windowIndex= */ 3, /* positionMs= */ 0); + castPlayer.seekTo(/* mediaItemIndex= */ 3, /* positionMs= */ 0); verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousWindow); verify(mockListener, times(3)).onAvailableCommandsChanged(any()); } @@ -1509,14 +1519,14 @@ public void seekTo_previousWindow_notifiesAvailableCommandsChanged() { // Check that there were no other calls to onAvailableCommandsChanged. verify(mockListener).onAvailableCommandsChanged(any()); - castPlayer.seekTo(/* windowIndex= */ 2, /* positionMs= */ 0); + castPlayer.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ 0); verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousAndNextWindow); verify(mockListener, times(2)).onAvailableCommandsChanged(any()); - castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0); + castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 0); verify(mockListener, times(2)).onAvailableCommandsChanged(any()); - castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0); + castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 0); verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToNextWindow); verify(mockListener, times(3)).onAvailableCommandsChanged(any()); } @@ -1533,8 +1543,8 @@ public void seekTo_sameWindow_doesNotNotifyAvailableCommandsChanged() { updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); verify(mockListener).onAvailableCommandsChanged(defaultCommands); - castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 200); - castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 100); + castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 200); + castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 100); // Check that there were no other calls to onAvailableCommandsChanged. verify(mockListener).onAvailableCommandsChanged(any()); } @@ -1782,6 +1792,7 @@ private List createMediaItems(int[] mediaQueueItemIds) { private MediaItem createMediaItem(int mediaQueueItemId) { return new MediaItem.Builder() .setUri("http://www.google.com/video" + mediaQueueItemId) + .setMediaMetadata(new MediaMetadata.Builder().setArtist("Foo Bar").build()) .setMimeType(MimeTypes.APPLICATION_MPD) .setTag(mediaQueueItemId) .build(); @@ -1821,8 +1832,12 @@ private void updateTimeLine( int mediaQueueItemId = mediaQueueItemIds[i]; int streamType = streamTypes[i]; long durationMs = durationsMs[i]; + String contentId = + mediaItem.mediaId.equals(MediaItem.DEFAULT_MEDIA_ID) + ? mediaItem.localConfiguration.uri.toString() + : mediaItem.mediaId; MediaInfo.Builder mediaInfoBuilder = - new MediaInfo.Builder(mediaItem.localConfiguration.uri.toString()) + new MediaInfo.Builder(contentId) .setStreamType(streamType) .setContentType(mediaItem.localConfiguration.mimeType); if (durationMs != C.TIME_UNSET) { diff --git a/libraries/cast/src/test/java/androidx/media3/cast/CastTimelineTrackerTest.java b/libraries/cast/src/test/java/androidx/media3/cast/CastTimelineTrackerTest.java index 20fe12ac45..42747462a8 100644 --- a/libraries/cast/src/test/java/androidx/media3/cast/CastTimelineTrackerTest.java +++ b/libraries/cast/src/test/java/androidx/media3/cast/CastTimelineTrackerTest.java @@ -15,21 +15,30 @@ */ package androidx.media3.cast; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import androidx.media3.common.C; +import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; +import androidx.media3.common.Timeline.Window; import androidx.media3.common.util.Util; import androidx.media3.test.utils.TimelineAsserts; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.framework.media.MediaQueue; import com.google.android.gms.cast.framework.media.RemoteMediaClient; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.List; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mockito; /** Tests for {@link CastTimelineTracker}. */ @RunWith(AndroidJUnit4.class) @@ -40,10 +49,19 @@ public class CastTimelineTrackerTest { private static final long DURATION_4_MS = 4000; private static final long DURATION_5_MS = 5000; + private MediaItemConverter mediaItemConverter; + private CastTimelineTracker castTimelineTracker; + + @Before + public void init() { + mediaItemConverter = new DefaultMediaItemConverter(); + castTimelineTracker = new CastTimelineTracker(mediaItemConverter); + } + /** Tests that duration of the current media info is correctly propagated to the timeline. */ @Test public void getCastTimelinePersistsDuration() { - CastTimelineTracker tracker = new CastTimelineTracker(); + CastTimelineTracker tracker = new CastTimelineTracker(new DefaultMediaItemConverter()); RemoteMediaClient remoteMediaClient = mockRemoteMediaClient( @@ -104,10 +122,179 @@ public void getCastTimelinePersistsDuration() { Util.msToUs(DURATION_5_MS)); } + @Test + public void getCastTimeline_onMediaItemsSet_correctMediaItemsInTimeline() { + RemoteMediaClient mockRemoteMediaClient = mock(RemoteMediaClient.class); + MediaQueue mockMediaQueue = mock(MediaQueue.class); + MediaStatus mockMediaStatus = mock(MediaStatus.class); + ImmutableList playlistMediaItems = + ImmutableList.of(createMediaItem(0), createMediaItem(1)); + MediaQueueItem[] playlistMediaQueueItems = + new MediaQueueItem[] { + createMediaQueueItem(playlistMediaItems.get(0), 0), + createMediaQueueItem(playlistMediaItems.get(1), 1) + }; + castTimelineTracker.onMediaItemsSet(playlistMediaItems, playlistMediaQueueItems); + // Mock remote media client state after adding two items. + when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue); + when(mockMediaQueue.getItemIds()).thenReturn(new int[] {0, 1}); + when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus); + when(mockMediaStatus.getCurrentItemId()).thenReturn(0); + when(mockMediaStatus.getMediaInfo()).thenReturn(playlistMediaQueueItems[0].getMedia()); + when(mockMediaStatus.getQueueItems()).thenReturn(Arrays.asList(playlistMediaQueueItems)); + + CastTimeline castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient); + + assertThat(castTimeline.getWindowCount()).isEqualTo(2); + assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem) + .isEqualTo(playlistMediaItems.get(0)); + assertThat(castTimeline.getWindow(/* windowIndex= */ 1, new Window()).mediaItem) + .isEqualTo(playlistMediaItems.get(1)); + + MediaItem thirdMediaItem = createMediaItem(2); + MediaQueueItem thirdMediaQueueItem = createMediaQueueItem(thirdMediaItem, 2); + castTimelineTracker.onMediaItemsSet( + ImmutableList.of(thirdMediaItem), new MediaQueueItem[] {thirdMediaQueueItem}); + // Mock remote media client state after a single item overrides the previous playlist. + when(mockMediaQueue.getItemIds()).thenReturn(new int[] {2}); + when(mockMediaStatus.getCurrentItemId()).thenReturn(2); + when(mockMediaStatus.getMediaInfo()).thenReturn(thirdMediaQueueItem.getMedia()); + when(mockMediaStatus.getQueueItems()).thenReturn(ImmutableList.of(thirdMediaQueueItem)); + + castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient); + + assertThat(castTimeline.getWindowCount()).isEqualTo(1); + assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem) + .isEqualTo(thirdMediaItem); + } + + @Test + public void getCastTimeline_onMediaItemsAdded_correctMediaItemsInTimeline() { + RemoteMediaClient mockRemoteMediaClient = mock(RemoteMediaClient.class); + MediaQueue mockMediaQueue = mock(MediaQueue.class); + MediaStatus mockMediaStatus = mock(MediaStatus.class); + ImmutableList playlistMediaItems = + ImmutableList.of(createMediaItem(0), createMediaItem(1)); + MediaQueueItem[] playlistQueueItems = + new MediaQueueItem[] { + createMediaQueueItem(playlistMediaItems.get(0), /* uid= */ 0), + createMediaQueueItem(playlistMediaItems.get(1), /* uid= */ 1) + }; + ImmutableList secondPlaylistMediaItems = + new ImmutableList.Builder() + .addAll(playlistMediaItems) + .add(createMediaItem(2)) + .build(); + castTimelineTracker.onMediaItemsAdded(playlistMediaItems, playlistQueueItems); + when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue); + when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus); + // Mock remote media client state after two items have been added. + when(mockMediaQueue.getItemIds()).thenReturn(new int[] {0, 1}); + when(mockMediaStatus.getCurrentItemId()).thenReturn(0); + when(mockMediaStatus.getMediaInfo()).thenReturn(playlistQueueItems[0].getMedia()); + when(mockMediaStatus.getQueueItems()).thenReturn(Arrays.asList(playlistQueueItems)); + + CastTimeline castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient); + + assertThat(castTimeline.getWindowCount()).isEqualTo(2); + assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem) + .isEqualTo(playlistMediaItems.get(0)); + assertThat(castTimeline.getWindow(/* windowIndex= */ 1, new Window()).mediaItem) + .isEqualTo(playlistMediaItems.get(1)); + + // Mock remote media client state after adding a third item. + List playlistThreeQueueItems = + new ArrayList<>(Arrays.asList(playlistQueueItems)); + playlistThreeQueueItems.add(createMediaQueueItem(secondPlaylistMediaItems.get(2), 2)); + castTimelineTracker.onMediaItemsAdded( + secondPlaylistMediaItems, playlistThreeQueueItems.toArray(new MediaQueueItem[0])); + when(mockMediaQueue.getItemIds()).thenReturn(new int[] {0, 1, 2}); + when(mockMediaStatus.getQueueItems()).thenReturn(playlistThreeQueueItems); + + castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient); + + assertThat(castTimeline.getWindowCount()).isEqualTo(3); + assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem) + .isEqualTo(secondPlaylistMediaItems.get(0)); + assertThat(castTimeline.getWindow(/* windowIndex= */ 1, new Window()).mediaItem) + .isEqualTo(secondPlaylistMediaItems.get(1)); + assertThat(castTimeline.getWindow(/* windowIndex= */ 2, new Window()).mediaItem) + .isEqualTo(secondPlaylistMediaItems.get(2)); + } + + @Test + public void getCastTimeline_itemsRemoved_correctMediaItemsInTimelineAndMapCleanedUp() { + RemoteMediaClient mockRemoteMediaClient = mock(RemoteMediaClient.class); + MediaQueue mockMediaQueue = mock(MediaQueue.class); + MediaStatus mockMediaStatus = mock(MediaStatus.class); + ImmutableList playlistMediaItems = + ImmutableList.of(createMediaItem(0), createMediaItem(1)); + MediaQueueItem[] initialPlaylistTwoQueueItems = + new MediaQueueItem[] { + createMediaQueueItem(playlistMediaItems.get(0), 0), + createMediaQueueItem(playlistMediaItems.get(1), 1) + }; + castTimelineTracker.onMediaItemsSet(playlistMediaItems, initialPlaylistTwoQueueItems); + when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue); + when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus); + // Mock remote media client state with two items in the queue. + when(mockMediaQueue.getItemIds()).thenReturn(new int[] {0, 1}); + when(mockMediaStatus.getCurrentItemId()).thenReturn(0); + when(mockMediaStatus.getMediaInfo()).thenReturn(initialPlaylistTwoQueueItems[0].getMedia()); + when(mockMediaStatus.getQueueItems()).thenReturn(Arrays.asList(initialPlaylistTwoQueueItems)); + + CastTimeline castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient); + + assertThat(castTimeline.getWindowCount()).isEqualTo(2); + assertThat(castTimelineTracker.mediaItemsByContentId).hasSize(2); + + // Mock remote media client state after the first item has been removed. + when(mockMediaQueue.getItemIds()).thenReturn(new int[] {1}); + when(mockMediaStatus.getCurrentItemId()).thenReturn(1); + when(mockMediaStatus.getMediaInfo()).thenReturn(initialPlaylistTwoQueueItems[1].getMedia()); + when(mockMediaStatus.getQueueItems()) + .thenReturn(ImmutableList.of(initialPlaylistTwoQueueItems[1])); + + castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient); + + assertThat(castTimeline.getWindowCount()).isEqualTo(1); + assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem) + .isEqualTo(playlistMediaItems.get(1)); + // Assert that the removed item has been removed from the content ID map. + assertThat(castTimelineTracker.mediaItemsByContentId).hasSize(1); + + // Mock remote media client state for empty queue. + when(mockRemoteMediaClient.getMediaStatus()).thenReturn(null); + when(mockMediaQueue.getItemIds()).thenReturn(new int[0]); + when(mockMediaStatus.getCurrentItemId()).thenReturn(MediaQueueItem.INVALID_ITEM_ID); + when(mockMediaStatus.getMediaInfo()).thenReturn(null); + when(mockMediaStatus.getQueueItems()).thenReturn(ImmutableList.of()); + + castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient); + + assertThat(castTimeline.getWindowCount()).isEqualTo(0); + // Queue is not emptied when remote media client is empty. See [Internal ref: b/128825216]. + assertThat(castTimelineTracker.mediaItemsByContentId).hasSize(1); + } + + private MediaItem createMediaItem(int uid) { + return new MediaItem.Builder() + .setUri("http://www.google.com/" + uid) + .setMimeType(MimeTypes.AUDIO_MPEG) + .setTag(uid) + .build(); + } + + private MediaQueueItem createMediaQueueItem(MediaItem mediaItem, int uid) { + return new MediaQueueItem.Builder(mediaItemConverter.toMediaQueueItem(mediaItem)) + .setItemId(uid) + .build(); + } + private static RemoteMediaClient mockRemoteMediaClient( int[] itemIds, int currentItemId, long currentDurationMs) { - RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class); - MediaStatus status = Mockito.mock(MediaStatus.class); + RemoteMediaClient remoteMediaClient = mock(RemoteMediaClient.class); + MediaStatus status = mock(MediaStatus.class); when(status.getQueueItems()).thenReturn(Collections.emptyList()); when(remoteMediaClient.getMediaStatus()).thenReturn(status); when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs)); @@ -118,7 +305,7 @@ private static RemoteMediaClient mockRemoteMediaClient( } private static MediaQueue mockMediaQueue(int[] itemIds) { - MediaQueue mediaQueue = Mockito.mock(MediaQueue.class); + MediaQueue mediaQueue = mock(MediaQueue.class); when(mediaQueue.getItemIds()).thenReturn(itemIds); return mediaQueue; }