Commit 05e42ebb by bachinger Committed by Rohit Singh

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 02e1484e)
parent 36a99cb1
...@@ -196,7 +196,7 @@ public final class CastPlayer extends BasePlayer { ...@@ -196,7 +196,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();
...@@ -281,8 +281,7 @@ public final class CastPlayer extends BasePlayer { ...@@ -281,8 +281,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
...@@ -292,7 +291,7 @@ public final class CastPlayer extends BasePlayer { ...@@ -292,7 +291,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
...@@ -1018,14 +1017,13 @@ public final class CastPlayer extends BasePlayer { ...@@ -1018,14 +1017,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) {
...@@ -1036,34 +1034,35 @@ public final class CastPlayer extends BasePlayer { ...@@ -1036,34 +1034,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
......
...@@ -15,13 +15,13 @@ ...@@ -15,13 +15,13 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.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 com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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,
......
...@@ -15,14 +15,23 @@ ...@@ -15,14 +15,23 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import static com.google.android.exoplayer2.ext.cast.CastTimeline.ItemData.UNKNOWN_CONTENT_ID;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import android.util.SparseArray; import android.util.SparseArray;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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++;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment