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
This commit is contained in:
bachinger 2022-07-11 23:22:27 +00:00 committed by Rohit Singh
parent 7dc54efdb9
commit 30fbc3a27d
6 changed files with 436 additions and 133 deletions

View File

@ -35,6 +35,11 @@
* Leanback extension: * Leanback extension:
* Listen to `playWhenReady` changes in `LeanbackAdapter` * Listen to `playWhenReady` changes in `LeanbackAdapter`
([10420](https://github.com/google/ExoPlayer/issues/10420)). ([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) ### 1.0.0-beta01 (2022-06-16)

View File

@ -200,7 +200,7 @@ public final class CastPlayer extends BasePlayer {
this.mediaItemConverter = mediaItemConverter; this.mediaItemConverter = mediaItemConverter;
this.seekBackIncrementMs = seekBackIncrementMs; this.seekBackIncrementMs = seekBackIncrementMs;
this.seekForwardIncrementMs = seekForwardIncrementMs; this.seekForwardIncrementMs = seekForwardIncrementMs;
timelineTracker = new CastTimelineTracker(); timelineTracker = new CastTimelineTracker(mediaItemConverter);
period = new Timeline.Period(); period = new Timeline.Period();
statusListener = new StatusListener(); statusListener = new StatusListener();
seekResultCallback = new SeekResultCallback(); seekResultCallback = new SeekResultCallback();
@ -285,8 +285,7 @@ public final class CastPlayer extends BasePlayer {
@Override @Override
public void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs) { public void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
setMediaItemsInternal( setMediaItemsInternal(mediaItems, startIndex, startPositionMs, repeatMode.value);
toMediaQueueItems(mediaItems), startIndex, startPositionMs, repeatMode.value);
} }
@Override @Override
@ -296,7 +295,7 @@ public final class CastPlayer extends BasePlayer {
if (index < currentTimeline.getWindowCount()) { if (index < currentTimeline.getWindowCount()) {
uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid; uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid;
} }
addMediaItemsInternal(toMediaQueueItems(mediaItems), uid); addMediaItemsInternal(mediaItems, uid);
} }
@Override @Override
@ -1022,14 +1021,13 @@ public final class CastPlayer extends BasePlayer {
} }
} }
@Nullable private void setMediaItemsInternal(
private PendingResult<MediaChannelResult> setMediaItemsInternal( List<MediaItem> mediaItems,
MediaQueueItem[] mediaQueueItems,
int startIndex, int startIndex,
long startPositionMs, long startPositionMs,
@RepeatMode int repeatMode) { @RepeatMode int repeatMode) {
if (remoteMediaClient == null || mediaQueueItems.length == 0) { if (remoteMediaClient == null || mediaItems.isEmpty()) {
return null; return;
} }
startPositionMs = startPositionMs == C.TIME_UNSET ? 0 : startPositionMs; startPositionMs = startPositionMs == C.TIME_UNSET ? 0 : startPositionMs;
if (startIndex == C.INDEX_UNSET) { if (startIndex == C.INDEX_UNSET) {
@ -1040,34 +1038,35 @@ public final class CastPlayer extends BasePlayer {
if (!currentTimeline.isEmpty()) { if (!currentTimeline.isEmpty()) {
pendingMediaItemRemovalPosition = getCurrentPositionInfo(); pendingMediaItemRemovalPosition = getCurrentPositionInfo();
} }
return remoteMediaClient.queueLoad( MediaQueueItem[] mediaQueueItems = toMediaQueueItems(mediaItems);
timelineTracker.onMediaItemsSet(mediaItems, mediaQueueItems);
remoteMediaClient.queueLoad(
mediaQueueItems, mediaQueueItems,
min(startIndex, mediaQueueItems.length - 1), min(startIndex, mediaItems.size() - 1),
getCastRepeatMode(repeatMode), getCastRepeatMode(repeatMode),
startPositionMs, startPositionMs,
/* customData= */ null); /* customData= */ null);
} }
@Nullable private void addMediaItemsInternal(List<MediaItem> mediaItems, int uid) {
private PendingResult<MediaChannelResult> addMediaItemsInternal(MediaQueueItem[] items, int uid) {
if (remoteMediaClient == null || getMediaStatus() == null) { 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 void moveMediaItemsInternal(int[] uids, int fromIndex, int newIndex) {
private PendingResult<MediaChannelResult> moveMediaItemsInternal(
int[] uids, int fromIndex, int newIndex) {
if (remoteMediaClient == null || getMediaStatus() == null) { if (remoteMediaClient == null || getMediaStatus() == null) {
return null; return;
} }
int insertBeforeIndex = fromIndex < newIndex ? newIndex + uids.length : newIndex; int insertBeforeIndex = fromIndex < newIndex ? newIndex + uids.length : newIndex;
int insertBeforeItemId = MediaQueueItem.INVALID_ITEM_ID; int insertBeforeItemId = MediaQueueItem.INVALID_ITEM_ID;
if (insertBeforeIndex < currentTimeline.getWindowCount()) { if (insertBeforeIndex < currentTimeline.getWindowCount()) {
insertBeforeItemId = (int) currentTimeline.getWindow(insertBeforeIndex, window).uid; insertBeforeItemId = (int) currentTimeline.getWindow(insertBeforeIndex, window).uid;
} }
return remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null); remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null);
} }
@Nullable @Nullable

View File

@ -15,13 +15,13 @@
*/ */
package androidx.media3.cast; package androidx.media3.cast;
import android.net.Uri;
import android.util.SparseArray; import android.util.SparseArray;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline; import androidx.media3.common.Timeline;
import com.google.android.gms.cast.MediaInfo;
import java.util.Arrays; import java.util.Arrays;
/** A {@link Timeline} for Cast media queues. */ /** 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. */ /** Holds {@link Timeline} related data for a Cast media item. */
public static final class ItemData { public static final class ItemData {
/* package */ static final String UNKNOWN_CONTENT_ID = "UNKNOWN_CONTENT_ID";
/** Holds no media information. */ /** Holds no media information. */
public static final ItemData EMPTY = public static final ItemData EMPTY =
new ItemData( new ItemData(
/* durationUs= */ C.TIME_UNSET, /* durationUs= */ C.TIME_UNSET,
/* defaultPositionUs= */ 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. */ /** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */
public final long durationUs; public final long durationUs;
@ -45,6 +49,10 @@ import java.util.Arrays;
public final long defaultPositionUs; public final long defaultPositionUs;
/** Whether the item is live content, or {@code false} if unknown. */ /** Whether the item is live content, or {@code false} if unknown. */
public final boolean isLive; 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. * Creates an instance.
@ -52,11 +60,20 @@ import java.util.Arrays;
* @param durationUs See {@link #durationsUs}. * @param durationUs See {@link #durationsUs}.
* @param defaultPositionUs See {@link #defaultPositionUs}. * @param defaultPositionUs See {@link #defaultPositionUs}.
* @param isLive See {@link #isLive}. * @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.durationUs = durationUs;
this.defaultPositionUs = defaultPositionUs; this.defaultPositionUs = defaultPositionUs;
this.isLive = isLive; 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} * @param defaultPositionUs The default start position in microseconds, or {@link C#TIME_UNSET}
* if unknown. * if unknown.
* @param isLive Whether the item is live, or {@code false} 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 if (durationUs == this.durationUs
&& defaultPositionUs == this.defaultPositionUs && defaultPositionUs == this.defaultPositionUs
&& isLive == this.isLive) { && isLive == this.isLive
&& contentId.equals(this.contentId)
&& mediaItem.equals(this.mediaItem)) {
return this; 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<>()); new CastTimeline(new int[0], new SparseArray<>());
private final SparseIntArray idsToIndex; private final SparseIntArray idsToIndex;
private final MediaItem[] mediaItems;
private final int[] ids; private final int[] ids;
private final long[] durationsUs; private final long[] durationsUs;
private final long[] defaultPositionsUs; private final long[] defaultPositionsUs;
@ -100,10 +127,12 @@ import java.util.Arrays;
durationsUs = new long[itemCount]; durationsUs = new long[itemCount];
defaultPositionsUs = new long[itemCount]; defaultPositionsUs = new long[itemCount];
isLive = new boolean[itemCount]; isLive = new boolean[itemCount];
mediaItems = new MediaItem[itemCount];
for (int i = 0; i < ids.length; i++) { for (int i = 0; i < ids.length; i++) {
int id = ids[i]; int id = ids[i];
idsToIndex.put(id, i); idsToIndex.put(id, i);
ItemData data = itemIdToData.get(id, ItemData.EMPTY); ItemData data = itemIdToData.get(id, ItemData.EMPTY);
mediaItems[i] = data.mediaItem.buildUpon().setTag(id).build();
durationsUs[i] = data.durationUs; durationsUs[i] = data.durationUs;
defaultPositionsUs[i] = data.defaultPositionUs == C.TIME_UNSET ? 0 : data.defaultPositionUs; defaultPositionsUs[i] = data.defaultPositionUs == C.TIME_UNSET ? 0 : data.defaultPositionUs;
isLive[i] = data.isLive; isLive[i] = data.isLive;
@ -121,18 +150,16 @@ import java.util.Arrays;
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
long durationUs = durationsUs[windowIndex]; long durationUs = durationsUs[windowIndex];
boolean isDynamic = durationUs == C.TIME_UNSET; boolean isDynamic = durationUs == C.TIME_UNSET;
MediaItem mediaItem =
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build();
return window.set( return window.set(
/* uid= */ ids[windowIndex], /* uid= */ ids[windowIndex],
/* mediaItem= */ mediaItem, /* mediaItem= */ mediaItems[windowIndex],
/* manifest= */ null, /* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET, /* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* isSeekable= */ !isDynamic, /* isSeekable= */ !isDynamic,
isDynamic, isDynamic,
isLive[windowIndex] ? mediaItem.liveConfiguration : null, isLive[windowIndex] ? mediaItems[windowIndex].liveConfiguration : null,
defaultPositionsUs[windowIndex], defaultPositionsUs[windowIndex],
durationUs, durationUs,
/* firstPeriodIndex= */ windowIndex, /* firstPeriodIndex= */ windowIndex,

View File

@ -15,14 +15,23 @@
*/ */
package androidx.media3.cast; 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 android.util.SparseArray;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C; 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.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.framework.media.RemoteMediaClient; import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
/** /**
* Creates {@link CastTimeline CastTimelines} from cast receiver app status updates. * Creates {@link CastTimeline CastTimelines} from cast receiver app status updates.
@ -33,9 +42,47 @@ import java.util.HashSet;
/* package */ final class CastTimelineTracker { /* package */ final class CastTimelineTracker {
private final SparseArray<CastTimeline.ItemData> itemIdToData; 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<>(); 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(); int currentItemId = mediaStatus.getCurrentItemId();
String currentContentId = checkStateNotNull(mediaStatus.getMediaInfo()).getContentId();
MediaItem mediaItem = mediaItemsByContentId.get(currentContentId);
updateItemData( 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()) { for (MediaQueueItem queueItem : mediaStatus.getQueueItems()) {
long defaultPositionUs = (long) (item.getStartTime() * C.MICROS_PER_SECOND); long defaultPositionUs = (long) (queueItem.getStartTime() * C.MICROS_PER_SECOND);
updateItemData(item.getItemId(), item.getMedia(), defaultPositionUs); @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); 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); CastTimeline.ItemData previousData = itemIdToData.get(itemId, CastTimeline.ItemData.EMPTY);
long durationUs = CastUtils.getStreamDurationUs(mediaInfo); long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
if (durationUs == C.TIME_UNSET) { if (durationUs == C.TIME_UNSET) {
@ -87,7 +152,10 @@ import java.util.HashSet;
if (defaultPositionUs == C.TIME_UNSET) { if (defaultPositionUs == C.TIME_UNSET) {
defaultPositionUs = previousData.defaultPositionUs; 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) { private void removeUnusedItemDataEntries(int[] itemIds) {
@ -99,6 +167,8 @@ import java.util.HashSet;
int index = 0; int index = 0;
while (index < itemIdToData.size()) { while (index < itemIdToData.size()) {
if (!scratchItemIds.contains(itemIdToData.keyAt(index))) { if (!scratchItemIds.contains(itemIdToData.keyAt(index))) {
CastTimeline.ItemData itemData = itemIdToData.valueAt(index);
mediaItemsByContentId.remove(itemData.contentId);
itemIdToData.removeAt(index); itemIdToData.removeAt(index);
} else { } else {
index++; index++;

View File

@ -63,6 +63,7 @@ import static org.mockito.MockitoAnnotations.initMocks;
import android.net.Uri; import android.net.Uri;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackParameters; import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player; import androidx.media3.common.Player;
@ -126,6 +127,7 @@ public class CastPlayerTest {
when(mockCastSession.getRemoteMediaClient()).thenReturn(mockRemoteMediaClient); when(mockCastSession.getRemoteMediaClient()).thenReturn(mockRemoteMediaClient);
when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus); when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus);
when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue); when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue);
when(mockMediaStatus.getMediaInfo()).thenReturn(new MediaInfo.Builder("contentId").build());
when(mockMediaQueue.getItemIds()).thenReturn(new int[0]); when(mockMediaQueue.getItemIds()).thenReturn(new int[0]);
// Make the remote media client present the same default values as ExoPlayer: // Make the remote media client present the same default values as ExoPlayer:
when(mockRemoteMediaClient.isPaused()).thenReturn(true); when(mockRemoteMediaClient.isPaused()).thenReturn(true);
@ -388,7 +390,7 @@ public class CastPlayerTest {
mediaItems.add( mediaItems.add(
new MediaItem.Builder().setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build()); 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) verify(mockRemoteMediaClient)
.queueLoad(queueItemsArgumentCaptor.capture(), eq(1), anyInt(), eq(2000L), any()); .queueLoad(queueItemsArgumentCaptor.capture(), eq(1), anyInt(), eq(2000L), any());
@ -424,32 +426,42 @@ public class CastPlayerTest {
String uri1 = "http://www.google.com/video1"; String uri1 = "http://www.google.com/video1";
String uri2 = "http://www.google.com/video2"; String uri2 = "http://www.google.com/video2";
firstPlaylist.add( 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( 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<MediaItem> secondPlaylist =
ImmutableList.of( ImmutableList.of(
new MediaItem.Builder() new MediaItem.Builder()
.setUri(Uri.EMPTY) .setUri(Uri.EMPTY)
.setTag(3)
.setMimeType(MimeTypes.APPLICATION_MPD) .setMimeType(MimeTypes.APPLICATION_MPD)
.build()); .build());
castPlayer.setMediaItems( castPlayer.setMediaItems(firstPlaylist, /* startIndex= */ 1, /* startPositionMs= */ 2000L);
firstPlaylist, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L);
updateTimeLine( updateTimeLine(
firstPlaylist, /* mediaQueueItemIds= */ new int[] {1, 2}, /* currentItemId= */ 2); firstPlaylist, /* mediaQueueItemIds= */ new int[] {1, 2}, /* currentItemId= */ 2);
// Replacing existing playlist. // Replacing existing playlist.
castPlayer.setMediaItems( castPlayer.setMediaItems(secondPlaylist, /* startIndex= */ 0, /* startPositionMs= */ 1000L);
secondPlaylist, /* startWindowIndex= */ 0, /* startPositionMs= */ 1000L);
updateTimeLine(secondPlaylist, /* mediaQueueItemIds= */ new int[] {3}, /* currentItemId= */ 3); updateTimeLine(secondPlaylist, /* mediaQueueItemIds= */ new int[] {3}, /* currentItemId= */ 3);
InOrder inOrder = Mockito.inOrder(mockListener); InOrder inOrder = Mockito.inOrder(mockListener);
inOrder inOrder
.verify(mockListener, times(2)) .verify(mockListener)
.onMediaItemTransition( .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()); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
assertThat(mediaItemCaptor.getAllValues().get(1).localConfiguration.tag).isEqualTo(3);
} }
@SuppressWarnings("deprecation") // Verifies deprecated callback being called correctly. @SuppressWarnings("deprecation") // Verifies deprecated callback being called correctly.
@ -459,18 +471,26 @@ public class CastPlayerTest {
String uri1 = "http://www.google.com/video1"; String uri1 = "http://www.google.com/video1";
String uri2 = "http://www.google.com/video2"; String uri2 = "http://www.google.com/video2";
firstPlaylist.add( 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( 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<MediaItem> secondPlaylist =
ImmutableList.of( ImmutableList.of(
new MediaItem.Builder() new MediaItem.Builder()
.setUri(Uri.EMPTY) .setUri(Uri.EMPTY)
.setMimeType(MimeTypes.APPLICATION_MPD) .setMimeType(MimeTypes.APPLICATION_MPD)
.setTag(3)
.build()); .build());
castPlayer.setMediaItems( castPlayer.setMediaItems(firstPlaylist, /* startIndex= */ 1, /* startPositionMs= */ 2000L);
firstPlaylist, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L);
updateTimeLine( updateTimeLine(
firstPlaylist, firstPlaylist,
/* mediaQueueItemIds= */ new int[] {1, 2}, /* mediaQueueItemIds= */ new int[] {1, 2},
@ -481,8 +501,7 @@ public class CastPlayerTest {
/* durationsMs= */ new long[] {20_000, 20_000}, /* durationsMs= */ new long[] {20_000, 20_000},
/* positionMs= */ 2000L); /* positionMs= */ 2000L);
// Replacing existing playlist. // Replacing existing playlist.
castPlayer.setMediaItems( castPlayer.setMediaItems(secondPlaylist, /* startIndex= */ 0, /* startPositionMs= */ 1000L);
secondPlaylist, /* startWindowIndex= */ 0, /* startPositionMs= */ 1000L);
updateTimeLine( updateTimeLine(
secondPlaylist, secondPlaylist,
/* mediaQueueItemIds= */ new int[] {3}, /* mediaQueueItemIds= */ new int[] {3},
@ -494,8 +513,8 @@ public class CastPlayerTest {
Player.PositionInfo oldPosition = Player.PositionInfo oldPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 2, /* windowUid= */ 2,
/* windowIndex= */ 1, /* mediaItemIndex= */ 1,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(), firstPlaylist.get(1),
/* periodUid= */ 2, /* periodUid= */ 2,
/* periodIndex= */ 1, /* periodIndex= */ 1,
/* positionMs= */ 2000, /* positionMs= */ 2000,
@ -505,8 +524,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition = Player.PositionInfo newPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 3, /* windowUid= */ 3,
/* windowIndex= */ 0, /* mediaItemIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(3).build(), secondPlaylist.get(0),
/* periodUid= */ 3, /* periodUid= */ 3,
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* positionMs= */ 1000, /* positionMs= */ 1000,
@ -720,10 +739,8 @@ public class CastPlayerTest {
inOrder inOrder
.verify(mockListener) .verify(mockListener)
.onMediaItemTransition( .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()); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
assertThat(mediaItemCaptor.getValue().localConfiguration.tag)
.isEqualTo(mediaItem.localConfiguration.tag);
} }
@Test @Test
@ -742,7 +759,8 @@ public class CastPlayerTest {
InOrder inOrder = Mockito.inOrder(mockListener); InOrder inOrder = Mockito.inOrder(mockListener);
inOrder inOrder
.verify(mockListener) .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 inOrder
.verify(mockListener) .verify(mockListener)
.onMediaItemTransition( .onMediaItemTransition(
@ -776,8 +794,8 @@ public class CastPlayerTest {
Player.PositionInfo oldPosition = Player.PositionInfo oldPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 1, /* windowUid= */ 1,
/* windowIndex= */ 0, /* mediaItemIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(), mediaItems.get(0),
/* periodUid= */ 1, /* periodUid= */ 1,
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* positionMs= */ 1234, /* positionMs= */ 1234,
@ -787,7 +805,7 @@ public class CastPlayerTest {
Player.PositionInfo newPosition = Player.PositionInfo newPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ null, /* windowUid= */ null,
/* windowIndex= */ 0, /* mediaItemIndex= */ 0,
/* mediaItem= */ null, /* mediaItem= */ null,
/* periodUid= */ null, /* periodUid= */ null,
/* periodIndex= */ 0, /* periodIndex= */ 0,
@ -827,10 +845,8 @@ public class CastPlayerTest {
.onMediaItemTransition( .onMediaItemTransition(
mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
assertThat(mediaItemCaptor.getAllValues().get(0).localConfiguration.tag) assertThat(mediaItemCaptor.getAllValues().get(0)).isEqualTo(mediaItem1);
.isEqualTo(mediaItem1.localConfiguration.tag); assertThat(mediaItemCaptor.getAllValues().get(1)).isEqualTo(mediaItem2);
assertThat(mediaItemCaptor.getAllValues().get(1).localConfiguration.tag)
.isEqualTo(mediaItem2.localConfiguration.tag);
} }
@Test @Test
@ -862,8 +878,8 @@ public class CastPlayerTest {
Player.PositionInfo oldPosition = Player.PositionInfo oldPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 1, /* windowUid= */ 1,
/* windowIndex= */ 0, /* mediaItemIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(), mediaItem1,
/* periodUid= */ 1, /* periodUid= */ 1,
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* positionMs= */ 1234, /* positionMs= */ 1234,
@ -873,8 +889,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition = Player.PositionInfo newPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 2, /* windowUid= */ 2,
/* windowIndex= */ 0, /* mediaItemIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(), mediaItem2,
/* periodUid= */ 2, /* periodUid= */ 2,
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* positionMs= */ 0, /* positionMs= */ 0,
@ -912,10 +928,8 @@ public class CastPlayerTest {
mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
List<MediaItem> capturedMediaItems = mediaItemCaptor.getAllValues(); List<MediaItem> capturedMediaItems = mediaItemCaptor.getAllValues();
assertThat(capturedMediaItems.get(0).localConfiguration.tag) assertThat(capturedMediaItems.get(0)).isEqualTo(mediaItem1);
.isEqualTo(mediaItem1.localConfiguration.tag); assertThat(capturedMediaItems.get(1)).isEqualTo(mediaItem2);
assertThat(capturedMediaItems.get(1).localConfiguration.tag)
.isEqualTo(mediaItem2.localConfiguration.tag);
} }
@Test @Test
@ -945,8 +959,8 @@ public class CastPlayerTest {
Player.PositionInfo oldPosition = Player.PositionInfo oldPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 1, /* windowUid= */ 1,
/* windowIndex= */ 0, /* mediaItemIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(), mediaItem1,
/* periodUid= */ 1, /* periodUid= */ 1,
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* positionMs= */ 0, // position at which we receive the timeline change /* positionMs= */ 0, // position at which we receive the timeline change
@ -956,8 +970,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition = Player.PositionInfo newPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 2, /* windowUid= */ 2,
/* windowIndex= */ 0, /* mediaItemIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(), mediaItem2,
/* periodUid= */ 2, /* periodUid= */ 2,
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* positionMs= */ 0, /* positionMs= */ 0,
@ -992,7 +1006,8 @@ public class CastPlayerTest {
InOrder inOrder = Mockito.inOrder(mockListener); InOrder inOrder = Mockito.inOrder(mockListener);
inOrder inOrder
.verify(mockListener) .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()); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
} }
@ -1027,19 +1042,17 @@ public class CastPlayerTest {
castPlayer.addMediaItems(mediaItems); castPlayer.addMediaItems(mediaItems);
updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1234); castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 1234);
InOrder inOrder = Mockito.inOrder(mockListener); InOrder inOrder = Mockito.inOrder(mockListener);
inOrder inOrder
.verify(mockListener) .verify(mockListener)
.onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); .onMediaItemTransition(
eq(mediaItem1), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
inOrder inOrder
.verify(mockListener) .verify(mockListener)
.onMediaItemTransition( .onMediaItemTransition(eq(mediaItem2), eq(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK));
mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK));
inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt()); inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
assertThat(mediaItemCaptor.getValue().localConfiguration.tag)
.isEqualTo(mediaItem2.localConfiguration.tag);
} }
@Test @Test
@ -1054,13 +1067,13 @@ public class CastPlayerTest {
castPlayer.addMediaItems(mediaItems); castPlayer.addMediaItems(mediaItems);
updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1234); castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 1234);
Player.PositionInfo oldPosition = Player.PositionInfo oldPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 1, /* windowUid= */ 1,
/* windowIndex= */ 0, /* mediaItemIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(), mediaItem1,
/* periodUid= */ 1, /* periodUid= */ 1,
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* positionMs= */ 0, /* positionMs= */ 0,
@ -1070,8 +1083,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition = Player.PositionInfo newPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 2, /* windowUid= */ 2,
/* windowIndex= */ 1, /* mediaItemIndex= */ 1,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(), mediaItem2,
/* periodUid= */ 2, /* periodUid= */ 2,
/* periodIndex= */ 1, /* periodIndex= */ 1,
/* positionMs= */ 1234, /* positionMs= */ 1234,
@ -1097,12 +1110,13 @@ public class CastPlayerTest {
castPlayer.addMediaItems(mediaItems); castPlayer.addMediaItems(mediaItems);
updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1234); castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 1234);
InOrder inOrder = Mockito.inOrder(mockListener); InOrder inOrder = Mockito.inOrder(mockListener);
inOrder inOrder
.verify(mockListener) .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()); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
} }
@ -1115,14 +1129,13 @@ public class CastPlayerTest {
castPlayer.addMediaItems(mediaItems); castPlayer.addMediaItems(mediaItems);
updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); 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 = Player.PositionInfo oldPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 1, /* windowUid= */ 1,
/* windowIndex= */ 0, /* mediaItemIndex= */ 0,
mediaItem, mediaItems.get(0),
/* periodUid= */ 1, /* periodUid= */ 1,
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* positionMs= */ 0, /* positionMs= */ 0,
@ -1132,8 +1145,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition = Player.PositionInfo newPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 1, /* windowUid= */ 1,
/* windowIndex= */ 0, /* mediaItemIndex= */ 0,
mediaItem, mediaItems.get(0),
/* periodUid= */ 1, /* periodUid= */ 1,
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* positionMs= */ 1234, /* positionMs= */ 1234,
@ -1164,13 +1177,12 @@ public class CastPlayerTest {
InOrder inOrder = Mockito.inOrder(mockListener); InOrder inOrder = Mockito.inOrder(mockListener);
inOrder inOrder
.verify(mockListener) .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 inOrder
.verify(mockListener) .verify(mockListener)
.onMediaItemTransition( .onMediaItemTransition(eq(mediaItems.get(1)), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO));
mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO));
inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
assertThat(mediaItemCaptor.getValue().localConfiguration.tag).isEqualTo(2);
} }
@Test @Test
@ -1203,8 +1215,8 @@ public class CastPlayerTest {
Player.PositionInfo oldPosition = Player.PositionInfo oldPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 1, /* windowUid= */ 1,
/* windowIndex= */ 0, /* mediaItemIndex= */ 0,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(), mediaItems.get(0),
/* periodUid= */ 1, /* periodUid= */ 1,
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* positionMs= */ 12500, /* positionMs= */ 12500,
@ -1214,8 +1226,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition = Player.PositionInfo newPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 2, /* windowUid= */ 2,
/* windowIndex= */ 1, /* mediaItemIndex= */ 1,
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(), mediaItems.get(1),
/* periodUid= */ 2, /* periodUid= */ 2,
/* periodIndex= */ 1, /* periodIndex= */ 1,
/* positionMs= */ 0, /* positionMs= */ 0,
@ -1250,12 +1262,11 @@ public class CastPlayerTest {
mediaItems, mediaQueueItemIds, currentItemId, streamTypes, durationsMs, positionMs); mediaItems, mediaQueueItemIds, currentItemId, streamTypes, durationsMs, positionMs);
castPlayer.seekBack(); castPlayer.seekBack();
MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build();
Player.PositionInfo oldPosition = Player.PositionInfo oldPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 1, /* windowUid= */ 1,
/* windowIndex= */ 0, /* mediaItemIndex= */ 0,
mediaItem, mediaItems.get(0),
/* periodUid= */ 1, /* periodUid= */ 1,
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* positionMs= */ 2 * C.DEFAULT_SEEK_BACK_INCREMENT_MS, /* positionMs= */ 2 * C.DEFAULT_SEEK_BACK_INCREMENT_MS,
@ -1265,8 +1276,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition = Player.PositionInfo newPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 1, /* windowUid= */ 1,
/* windowIndex= */ 0, /* mediaItemIndex= */ 0,
mediaItem, mediaItems.get(0),
/* periodUid= */ 1, /* periodUid= */ 1,
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* positionMs= */ C.DEFAULT_SEEK_BACK_INCREMENT_MS, /* positionMs= */ C.DEFAULT_SEEK_BACK_INCREMENT_MS,
@ -1299,12 +1310,11 @@ public class CastPlayerTest {
mediaItems, mediaQueueItemIds, currentItemId, streamTypes, durationsMs, positionMs); mediaItems, mediaQueueItemIds, currentItemId, streamTypes, durationsMs, positionMs);
castPlayer.seekForward(); castPlayer.seekForward();
MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build();
Player.PositionInfo oldPosition = Player.PositionInfo oldPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 1, /* windowUid= */ 1,
/* windowIndex= */ 0, /* mediaItemIndex= */ 0,
mediaItem, mediaItems.get(0),
/* periodUid= */ 1, /* periodUid= */ 1,
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* positionMs= */ 0, /* positionMs= */ 0,
@ -1314,8 +1324,8 @@ public class CastPlayerTest {
Player.PositionInfo newPosition = Player.PositionInfo newPosition =
new Player.PositionInfo( new Player.PositionInfo(
/* windowUid= */ 1, /* windowUid= */ 1,
/* windowIndex= */ 0, /* mediaItemIndex= */ 0,
mediaItem, mediaItems.get(0),
/* periodUid= */ 1, /* periodUid= */ 1,
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* positionMs= */ C.DEFAULT_SEEK_FORWARD_INCREMENT_MS, /* positionMs= */ C.DEFAULT_SEEK_FORWARD_INCREMENT_MS,
@ -1475,14 +1485,14 @@ public class CastPlayerTest {
// Check that there were no other calls to onAvailableCommandsChanged. // Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any()); verify(mockListener).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0); castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousAndNextWindow); verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousAndNextWindow);
verify(mockListener, times(2)).onAvailableCommandsChanged(any()); verify(mockListener, times(2)).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 2, /* positionMs= */ 0); castPlayer.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ 0);
verify(mockListener, times(2)).onAvailableCommandsChanged(any()); verify(mockListener, times(2)).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 3, /* positionMs= */ 0); castPlayer.seekTo(/* mediaItemIndex= */ 3, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousWindow); verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousWindow);
verify(mockListener, times(3)).onAvailableCommandsChanged(any()); verify(mockListener, times(3)).onAvailableCommandsChanged(any());
} }
@ -1509,14 +1519,14 @@ public class CastPlayerTest {
// Check that there were no other calls to onAvailableCommandsChanged. // Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any()); verify(mockListener).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 2, /* positionMs= */ 0); castPlayer.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousAndNextWindow); verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousAndNextWindow);
verify(mockListener, times(2)).onAvailableCommandsChanged(any()); verify(mockListener, times(2)).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0); castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 0);
verify(mockListener, times(2)).onAvailableCommandsChanged(any()); verify(mockListener, times(2)).onAvailableCommandsChanged(any());
castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0); castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToNextWindow); verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToNextWindow);
verify(mockListener, times(3)).onAvailableCommandsChanged(any()); verify(mockListener, times(3)).onAvailableCommandsChanged(any());
} }
@ -1533,8 +1543,8 @@ public class CastPlayerTest {
updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
verify(mockListener).onAvailableCommandsChanged(defaultCommands); verify(mockListener).onAvailableCommandsChanged(defaultCommands);
castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 200); castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 200);
castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 100); castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 100);
// Check that there were no other calls to onAvailableCommandsChanged. // Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any()); verify(mockListener).onAvailableCommandsChanged(any());
} }
@ -1782,6 +1792,7 @@ public class CastPlayerTest {
private MediaItem createMediaItem(int mediaQueueItemId) { private MediaItem createMediaItem(int mediaQueueItemId) {
return new MediaItem.Builder() return new MediaItem.Builder()
.setUri("http://www.google.com/video" + mediaQueueItemId) .setUri("http://www.google.com/video" + mediaQueueItemId)
.setMediaMetadata(new MediaMetadata.Builder().setArtist("Foo Bar").build())
.setMimeType(MimeTypes.APPLICATION_MPD) .setMimeType(MimeTypes.APPLICATION_MPD)
.setTag(mediaQueueItemId) .setTag(mediaQueueItemId)
.build(); .build();
@ -1821,8 +1832,12 @@ public class CastPlayerTest {
int mediaQueueItemId = mediaQueueItemIds[i]; int mediaQueueItemId = mediaQueueItemIds[i];
int streamType = streamTypes[i]; int streamType = streamTypes[i];
long durationMs = durationsMs[i]; long durationMs = durationsMs[i];
String contentId =
mediaItem.mediaId.equals(MediaItem.DEFAULT_MEDIA_ID)
? mediaItem.localConfiguration.uri.toString()
: mediaItem.mediaId;
MediaInfo.Builder mediaInfoBuilder = MediaInfo.Builder mediaInfoBuilder =
new MediaInfo.Builder(mediaItem.localConfiguration.uri.toString()) new MediaInfo.Builder(contentId)
.setStreamType(streamType) .setStreamType(streamType)
.setContentType(mediaItem.localConfiguration.mimeType); .setContentType(mediaItem.localConfiguration.mimeType);
if (durationMs != C.TIME_UNSET) { if (durationMs != C.TIME_UNSET) {

View File

@ -15,21 +15,30 @@
*/ */
package androidx.media3.cast; 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 static org.mockito.Mockito.when;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.Timeline.Window;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.test.utils.TimelineAsserts; import androidx.media3.test.utils.TimelineAsserts;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.gms.cast.MediaInfo; 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.MediaStatus;
import com.google.android.gms.cast.framework.media.MediaQueue; import com.google.android.gms.cast.framework.media.MediaQueue;
import com.google.android.gms.cast.framework.media.RemoteMediaClient; 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.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito;
/** Tests for {@link CastTimelineTracker}. */ /** Tests for {@link CastTimelineTracker}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@ -40,10 +49,19 @@ public class CastTimelineTrackerTest {
private static final long DURATION_4_MS = 4000; private static final long DURATION_4_MS = 4000;
private static final long DURATION_5_MS = 5000; 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. */ /** Tests that duration of the current media info is correctly propagated to the timeline. */
@Test @Test
public void getCastTimelinePersistsDuration() { public void getCastTimelinePersistsDuration() {
CastTimelineTracker tracker = new CastTimelineTracker(); CastTimelineTracker tracker = new CastTimelineTracker(new DefaultMediaItemConverter());
RemoteMediaClient remoteMediaClient = RemoteMediaClient remoteMediaClient =
mockRemoteMediaClient( mockRemoteMediaClient(
@ -104,10 +122,179 @@ public class CastTimelineTrackerTest {
Util.msToUs(DURATION_5_MS)); 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( private static RemoteMediaClient mockRemoteMediaClient(
int[] itemIds, int currentItemId, long currentDurationMs) { int[] itemIds, int currentItemId, long currentDurationMs) {
RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class); RemoteMediaClient remoteMediaClient = mock(RemoteMediaClient.class);
MediaStatus status = Mockito.mock(MediaStatus.class); MediaStatus status = mock(MediaStatus.class);
when(status.getQueueItems()).thenReturn(Collections.emptyList()); when(status.getQueueItems()).thenReturn(Collections.emptyList());
when(remoteMediaClient.getMediaStatus()).thenReturn(status); when(remoteMediaClient.getMediaStatus()).thenReturn(status);
when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs)); when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
@ -118,7 +305,7 @@ public class CastTimelineTrackerTest {
} }
private static MediaQueue mockMediaQueue(int[] itemIds) { private static MediaQueue mockMediaQueue(int[] itemIds) {
MediaQueue mediaQueue = Mockito.mock(MediaQueue.class); MediaQueue mediaQueue = mock(MediaQueue.class);
when(mediaQueue.getItemIds()).thenReturn(itemIds); when(mediaQueue.getItemIds()).thenReturn(itemIds);
return mediaQueue; return mediaQueue;
} }