Use the public MediaItem in the timeline of CastPlayer

The media item needs to be assigned to `Window.mediaItem` in `CastTimeline.setWindow`. For this the `MediaItem` needs to be available in the timeline.

When a `MediaItem` is passed to the `set/addMediaItems` method, we can't yet know the Cast `MediaQueueItem.itemId` that is generated on the device and arrives with an async update of the `RemoteMediaClient` state. Hence in the `CastTimelineTracker`, we need to store the `MediaItem` by Casts's `MediaItem.contentId`. When we then receive the updated queue, we look the media item up by the content ID to augment the `ItemData` that is available in the `CastTimeline`.

Issue: androidx/media#25
Issue: google/ExoPlayer#8212

#minor-release

PiperOrigin-RevId: 460325235
(cherry picked from commit 30fbc3a27d1c2b673c3f0a6f1c8956e183b11952)
This commit is contained in:
bachinger 2022-07-11 23:22:27 +00:00 committed by Rohit Singh
parent 329aa5864c
commit fe6baee775
6 changed files with 436 additions and 133 deletions

View File

@ -29,6 +29,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)

View File

@ -198,7 +198,7 @@ public final class CastPlayer extends BasePlayer {
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();
@ -283,8 +283,7 @@ public final class CastPlayer extends BasePlayer {
@Override
public void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
setMediaItemsInternal(
toMediaQueueItems(mediaItems), startIndex, startPositionMs, repeatMode.value);
setMediaItemsInternal(mediaItems, startIndex, startPositionMs, repeatMode.value);
}
@Override
@ -294,7 +293,7 @@ public final class CastPlayer extends BasePlayer {
if (index < currentTimeline.getWindowCount()) {
uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid;
}
addMediaItemsInternal(toMediaQueueItems(mediaItems), uid);
addMediaItemsInternal(mediaItems, uid);
}
@Override
@ -1020,14 +1019,13 @@ public final class CastPlayer extends BasePlayer {
}
}
@Nullable
private PendingResult<MediaChannelResult> setMediaItemsInternal(
MediaQueueItem[] mediaQueueItems,
private void setMediaItemsInternal(
List<MediaItem> 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) {
@ -1038,34 +1036,35 @@ public final class CastPlayer extends BasePlayer {
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<MediaChannelResult> addMediaItemsInternal(MediaQueueItem[] items, int uid) {
private void addMediaItemsInternal(List<MediaItem> 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<MediaChannelResult> 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

View File

@ -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 @@ import java.util.Arrays;
/** 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 @@ import java.util.Arrays;
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 @@ import java.util.Arrays;
* @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 @@ import java.util.Arrays;
* @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 @@ import java.util.Arrays;
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 @@ import java.util.Arrays;
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 @@ import java.util.Arrays;
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,

View File

@ -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 @@ import java.util.HashSet;
/* package */ final class CastTimelineTracker {
private final SparseArray<CastTimeline.ItemData> itemIdToData;
private final MediaItemConverter mediaItemConverter;
@VisibleForTesting /* package */ final HashMap<String, MediaItem> 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<MediaItem> 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<MediaItem> 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 @@ import java.util.HashSet;
}
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 @@ import java.util.HashSet;
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 @@ import java.util.HashSet;
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++;

View File

@ -63,6 +63,7 @@ import static org.mockito.MockitoAnnotations.initMocks;
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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<MediaItem> 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 class CastPlayerTest {
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<MediaItem> 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 class CastPlayerTest {
/* 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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
Player.PositionInfo newPosition =
new Player.PositionInfo(
/* windowUid= */ null,
/* windowIndex= */ 0,
/* mediaItemIndex= */ 0,
/* mediaItem= */ null,
/* periodUid= */ null,
/* periodIndex= */ 0,
@ -827,10 +845,8 @@ public class CastPlayerTest {
.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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
List<MediaItem> 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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
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 class CastPlayerTest {
// 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 class CastPlayerTest {
// 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 class CastPlayerTest {
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 @@ public class CastPlayerTest {
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 @@ public class CastPlayerTest {
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) {

View File

@ -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 class CastTimelineTrackerTest {
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<MediaItem> 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<MediaItem> playlistMediaItems =
ImmutableList.of(createMediaItem(0), createMediaItem(1));
MediaQueueItem[] playlistQueueItems =
new MediaQueueItem[] {
createMediaQueueItem(playlistMediaItems.get(0), /* uid= */ 0),
createMediaQueueItem(playlistMediaItems.get(1), /* uid= */ 1)
};
ImmutableList<MediaItem> secondPlaylistMediaItems =
new ImmutableList.Builder<MediaItem>()
.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<MediaQueueItem> 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<MediaItem> 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 @@ public class CastTimelineTrackerTest {
}
private static MediaQueue mockMediaQueue(int[] itemIds) {
MediaQueue mediaQueue = Mockito.mock(MediaQueue.class);
MediaQueue mediaQueue = mock(MediaQueue.class);
when(mediaQueue.getItemIds()).thenReturn(itemIds);
return mediaQueue;
}