Skip to content

Commit fe6baee

Browse files
marcbaechingerrohitjoins
authored andcommitted
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: #25 Issue: google/ExoPlayer#8212 #minor-release PiperOrigin-RevId: 460325235 (cherry picked from commit 30fbc3a)
1 parent 329aa58 commit fe6baee

6 files changed

Lines changed: 436 additions & 133 deletions

File tree

RELEASENOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
* Leanback extension:
3030
* Listen to `playWhenReady` changes in `LeanbackAdapter`
3131
([10420](https://github.com/google/ExoPlayer/issues/10420)).
32+
* Cast:
33+
* Use the `MediaItem` that has been passed to the playlist methods as
34+
`Window.mediaItem` in `CastTimeline`
35+
([#25](https://github.com/androidx/media/issues/25),
36+
[#8212](https://github.com/google/ExoPlayer/issues/8212)).
3237

3338
### 1.0.0-beta01 (2022-06-16)
3439

libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ public CastPlayer(
198198
this.mediaItemConverter = mediaItemConverter;
199199
this.seekBackIncrementMs = seekBackIncrementMs;
200200
this.seekForwardIncrementMs = seekForwardIncrementMs;
201-
timelineTracker = new CastTimelineTracker();
201+
timelineTracker = new CastTimelineTracker(mediaItemConverter);
202202
period = new Timeline.Period();
203203
statusListener = new StatusListener();
204204
seekResultCallback = new SeekResultCallback();
@@ -283,8 +283,7 @@ public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
283283

284284
@Override
285285
public void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
286-
setMediaItemsInternal(
287-
toMediaQueueItems(mediaItems), startIndex, startPositionMs, repeatMode.value);
286+
setMediaItemsInternal(mediaItems, startIndex, startPositionMs, repeatMode.value);
288287
}
289288

290289
@Override
@@ -294,7 +293,7 @@ public void addMediaItems(int index, List<MediaItem> mediaItems) {
294293
if (index < currentTimeline.getWindowCount()) {
295294
uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid;
296295
}
297-
addMediaItemsInternal(toMediaQueueItems(mediaItems), uid);
296+
addMediaItemsInternal(mediaItems, uid);
298297
}
299298

300299
@Override
@@ -1020,14 +1019,13 @@ private void updateAvailableCommandsAndNotifyIfChanged() {
10201019
}
10211020
}
10221021

1023-
@Nullable
1024-
private PendingResult<MediaChannelResult> setMediaItemsInternal(
1025-
MediaQueueItem[] mediaQueueItems,
1022+
private void setMediaItemsInternal(
1023+
List<MediaItem> mediaItems,
10261024
int startIndex,
10271025
long startPositionMs,
10281026
@RepeatMode int repeatMode) {
1029-
if (remoteMediaClient == null || mediaQueueItems.length == 0) {
1030-
return null;
1027+
if (remoteMediaClient == null || mediaItems.isEmpty()) {
1028+
return;
10311029
}
10321030
startPositionMs = startPositionMs == C.TIME_UNSET ? 0 : startPositionMs;
10331031
if (startIndex == C.INDEX_UNSET) {
@@ -1038,34 +1036,35 @@ private PendingResult<MediaChannelResult> setMediaItemsInternal(
10381036
if (!currentTimeline.isEmpty()) {
10391037
pendingMediaItemRemovalPosition = getCurrentPositionInfo();
10401038
}
1041-
return remoteMediaClient.queueLoad(
1039+
MediaQueueItem[] mediaQueueItems = toMediaQueueItems(mediaItems);
1040+
timelineTracker.onMediaItemsSet(mediaItems, mediaQueueItems);
1041+
remoteMediaClient.queueLoad(
10421042
mediaQueueItems,
1043-
min(startIndex, mediaQueueItems.length - 1),
1043+
min(startIndex, mediaItems.size() - 1),
10441044
getCastRepeatMode(repeatMode),
10451045
startPositionMs,
10461046
/* customData= */ null);
10471047
}
10481048

1049-
@Nullable
1050-
private PendingResult<MediaChannelResult> addMediaItemsInternal(MediaQueueItem[] items, int uid) {
1049+
private void addMediaItemsInternal(List<MediaItem> mediaItems, int uid) {
10511050
if (remoteMediaClient == null || getMediaStatus() == null) {
1052-
return null;
1051+
return;
10531052
}
1054-
return remoteMediaClient.queueInsertItems(items, uid, /* customData= */ null);
1053+
MediaQueueItem[] itemsToInsert = toMediaQueueItems(mediaItems);
1054+
timelineTracker.onMediaItemsAdded(mediaItems, itemsToInsert);
1055+
remoteMediaClient.queueInsertItems(itemsToInsert, uid, /* customData= */ null);
10551056
}
10561057

1057-
@Nullable
1058-
private PendingResult<MediaChannelResult> moveMediaItemsInternal(
1059-
int[] uids, int fromIndex, int newIndex) {
1058+
private void moveMediaItemsInternal(int[] uids, int fromIndex, int newIndex) {
10601059
if (remoteMediaClient == null || getMediaStatus() == null) {
1061-
return null;
1060+
return;
10621061
}
10631062
int insertBeforeIndex = fromIndex < newIndex ? newIndex + uids.length : newIndex;
10641063
int insertBeforeItemId = MediaQueueItem.INVALID_ITEM_ID;
10651064
if (insertBeforeIndex < currentTimeline.getWindowCount()) {
10661065
insertBeforeItemId = (int) currentTimeline.getWindow(insertBeforeIndex, window).uid;
10671066
}
1068-
return remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null);
1067+
remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null);
10691068
}
10701069

10711070
@Nullable

libraries/cast/src/main/java/androidx/media3/cast/CastTimeline.java

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
*/
1616
package androidx.media3.cast;
1717

18-
import android.net.Uri;
1918
import android.util.SparseArray;
2019
import android.util.SparseIntArray;
2120
import androidx.annotation.Nullable;
2221
import androidx.media3.common.C;
2322
import androidx.media3.common.MediaItem;
2423
import androidx.media3.common.Timeline;
24+
import com.google.android.gms.cast.MediaInfo;
2525
import java.util.Arrays;
2626

2727
/** A {@link Timeline} for Cast media queues. */
@@ -30,12 +30,16 @@
3030
/** Holds {@link Timeline} related data for a Cast media item. */
3131
public static final class ItemData {
3232

33+
/* package */ static final String UNKNOWN_CONTENT_ID = "UNKNOWN_CONTENT_ID";
34+
3335
/** Holds no media information. */
3436
public static final ItemData EMPTY =
3537
new ItemData(
3638
/* durationUs= */ C.TIME_UNSET,
3739
/* defaultPositionUs= */ C.TIME_UNSET,
38-
/* isLive= */ false);
40+
/* isLive= */ false,
41+
MediaItem.EMPTY,
42+
UNKNOWN_CONTENT_ID);
3943

4044
/** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */
4145
public final long durationUs;
@@ -45,18 +49,31 @@ public static final class ItemData {
4549
public final long defaultPositionUs;
4650
/** Whether the item is live content, or {@code false} if unknown. */
4751
public final boolean isLive;
52+
/** The original media item that has been set or added to the playlist. */
53+
public final MediaItem mediaItem;
54+
/** The {@linkplain MediaInfo#getContentId() content ID} of the cast media queue item. */
55+
public final String contentId;
4856

4957
/**
5058
* Creates an instance.
5159
*
5260
* @param durationUs See {@link #durationsUs}.
5361
* @param defaultPositionUs See {@link #defaultPositionUs}.
5462
* @param isLive See {@link #isLive}.
63+
* @param mediaItem See {@link #mediaItem}.
64+
* @param contentId See {@link #contentId}.
5565
*/
56-
public ItemData(long durationUs, long defaultPositionUs, boolean isLive) {
66+
public ItemData(
67+
long durationUs,
68+
long defaultPositionUs,
69+
boolean isLive,
70+
MediaItem mediaItem,
71+
String contentId) {
5772
this.durationUs = durationUs;
5873
this.defaultPositionUs = defaultPositionUs;
5974
this.isLive = isLive;
75+
this.mediaItem = mediaItem;
76+
this.contentId = contentId;
6077
}
6178

6279
/**
@@ -66,14 +83,23 @@ public ItemData(long durationUs, long defaultPositionUs, boolean isLive) {
6683
* @param defaultPositionUs The default start position in microseconds, or {@link C#TIME_UNSET}
6784
* if unknown.
6885
* @param isLive Whether the item is live, or {@code false} if unknown.
86+
* @param mediaItem The media item.
87+
* @param contentId The content ID.
6988
*/
70-
public ItemData copyWithNewValues(long durationUs, long defaultPositionUs, boolean isLive) {
89+
public ItemData copyWithNewValues(
90+
long durationUs,
91+
long defaultPositionUs,
92+
boolean isLive,
93+
MediaItem mediaItem,
94+
String contentId) {
7195
if (durationUs == this.durationUs
7296
&& defaultPositionUs == this.defaultPositionUs
73-
&& isLive == this.isLive) {
97+
&& isLive == this.isLive
98+
&& contentId.equals(this.contentId)
99+
&& mediaItem.equals(this.mediaItem)) {
74100
return this;
75101
}
76-
return new ItemData(durationUs, defaultPositionUs, isLive);
102+
return new ItemData(durationUs, defaultPositionUs, isLive, mediaItem, contentId);
77103
}
78104
}
79105

@@ -82,6 +108,7 @@ public ItemData copyWithNewValues(long durationUs, long defaultPositionUs, boole
82108
new CastTimeline(new int[0], new SparseArray<>());
83109

84110
private final SparseIntArray idsToIndex;
111+
private final MediaItem[] mediaItems;
85112
private final int[] ids;
86113
private final long[] durationsUs;
87114
private final long[] defaultPositionsUs;
@@ -100,10 +127,12 @@ public CastTimeline(int[] itemIds, SparseArray<ItemData> itemIdToData) {
100127
durationsUs = new long[itemCount];
101128
defaultPositionsUs = new long[itemCount];
102129
isLive = new boolean[itemCount];
130+
mediaItems = new MediaItem[itemCount];
103131
for (int i = 0; i < ids.length; i++) {
104132
int id = ids[i];
105133
idsToIndex.put(id, i);
106134
ItemData data = itemIdToData.get(id, ItemData.EMPTY);
135+
mediaItems[i] = data.mediaItem.buildUpon().setTag(id).build();
107136
durationsUs[i] = data.durationUs;
108137
defaultPositionsUs[i] = data.defaultPositionUs == C.TIME_UNSET ? 0 : data.defaultPositionUs;
109138
isLive[i] = data.isLive;
@@ -121,18 +150,16 @@ public int getWindowCount() {
121150
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
122151
long durationUs = durationsUs[windowIndex];
123152
boolean isDynamic = durationUs == C.TIME_UNSET;
124-
MediaItem mediaItem =
125-
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build();
126153
return window.set(
127154
/* uid= */ ids[windowIndex],
128-
/* mediaItem= */ mediaItem,
155+
/* mediaItem= */ mediaItems[windowIndex],
129156
/* manifest= */ null,
130157
/* presentationStartTimeMs= */ C.TIME_UNSET,
131158
/* windowStartTimeMs= */ C.TIME_UNSET,
132159
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
133160
/* isSeekable= */ !isDynamic,
134161
isDynamic,
135-
isLive[windowIndex] ? mediaItem.liveConfiguration : null,
162+
isLive[windowIndex] ? mediaItems[windowIndex].liveConfiguration : null,
136163
defaultPositionsUs[windowIndex],
137164
durationUs,
138165
/* firstPeriodIndex= */ windowIndex,

libraries/cast/src/main/java/androidx/media3/cast/CastTimelineTracker.java

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,23 @@
1515
*/
1616
package androidx.media3.cast;
1717

18+
import static androidx.media3.cast.CastTimeline.ItemData.UNKNOWN_CONTENT_ID;
19+
import static androidx.media3.common.util.Assertions.checkNotNull;
20+
import static androidx.media3.common.util.Assertions.checkStateNotNull;
21+
1822
import android.util.SparseArray;
1923
import androidx.annotation.Nullable;
24+
import androidx.annotation.VisibleForTesting;
2025
import androidx.media3.common.C;
26+
import androidx.media3.common.MediaItem;
27+
import androidx.media3.common.Player;
2128
import com.google.android.gms.cast.MediaInfo;
2229
import com.google.android.gms.cast.MediaQueueItem;
2330
import com.google.android.gms.cast.MediaStatus;
2431
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
32+
import java.util.HashMap;
2533
import java.util.HashSet;
34+
import java.util.List;
2635

2736
/**
2837
* Creates {@link CastTimeline CastTimelines} from cast receiver app status updates.
@@ -33,9 +42,47 @@
3342
/* package */ final class CastTimelineTracker {
3443

3544
private final SparseArray<CastTimeline.ItemData> itemIdToData;
45+
private final MediaItemConverter mediaItemConverter;
46+
@VisibleForTesting /* package */ final HashMap<String, MediaItem> mediaItemsByContentId;
3647

37-
public CastTimelineTracker() {
48+
/**
49+
* Creates an instance.
50+
*
51+
* @param mediaItemConverter The converter used to convert from a {@link MediaQueueItem} to a
52+
* {@link MediaItem}.
53+
*/
54+
public CastTimelineTracker(MediaItemConverter mediaItemConverter) {
55+
this.mediaItemConverter = mediaItemConverter;
3856
itemIdToData = new SparseArray<>();
57+
mediaItemsByContentId = new HashMap<>();
58+
}
59+
60+
/**
61+
* Called when media items {@linkplain Player#setMediaItems have been set to the playlist} and are
62+
* sent to the cast playback queue. A future queue update of the {@link RemoteMediaClient} will
63+
* reflect this addition.
64+
*
65+
* @param mediaItems The media items that have been set.
66+
* @param mediaQueueItems The corresponding media queue items.
67+
*/
68+
public void onMediaItemsSet(List<MediaItem> mediaItems, MediaQueueItem[] mediaQueueItems) {
69+
mediaItemsByContentId.clear();
70+
onMediaItemsAdded(mediaItems, mediaQueueItems);
71+
}
72+
73+
/**
74+
* Called when media items {@linkplain Player#addMediaItems(List) have been added} and are sent to
75+
* the cast playback queue. A future queue update of the {@link RemoteMediaClient} will reflect
76+
* this addition.
77+
*
78+
* @param mediaItems The media items that have been added.
79+
* @param mediaQueueItems The corresponding media queue items.
80+
*/
81+
public void onMediaItemsAdded(List<MediaItem> mediaItems, MediaQueueItem[] mediaQueueItems) {
82+
for (int i = 0; i < mediaItems.size(); i++) {
83+
mediaItemsByContentId.put(
84+
checkNotNull(mediaQueueItems[i].getMedia()).getContentId(), mediaItems.get(i));
85+
}
3986
}
4087

4188
/**
@@ -63,18 +110,36 @@ public CastTimeline getCastTimeline(RemoteMediaClient remoteMediaClient) {
63110
}
64111

65112
int currentItemId = mediaStatus.getCurrentItemId();
113+
String currentContentId = checkStateNotNull(mediaStatus.getMediaInfo()).getContentId();
114+
MediaItem mediaItem = mediaItemsByContentId.get(currentContentId);
66115
updateItemData(
67-
currentItemId, mediaStatus.getMediaInfo(), /* defaultPositionUs= */ C.TIME_UNSET);
116+
currentItemId,
117+
mediaItem != null ? mediaItem : MediaItem.EMPTY,
118+
mediaStatus.getMediaInfo(),
119+
currentContentId,
120+
/* defaultPositionUs= */ C.TIME_UNSET);
68121

69-
for (MediaQueueItem item : mediaStatus.getQueueItems()) {
70-
long defaultPositionUs = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
71-
updateItemData(item.getItemId(), item.getMedia(), defaultPositionUs);
122+
for (MediaQueueItem queueItem : mediaStatus.getQueueItems()) {
123+
long defaultPositionUs = (long) (queueItem.getStartTime() * C.MICROS_PER_SECOND);
124+
@Nullable MediaInfo mediaInfo = queueItem.getMedia();
125+
String contentId = mediaInfo != null ? mediaInfo.getContentId() : UNKNOWN_CONTENT_ID;
126+
mediaItem = mediaItemsByContentId.get(contentId);
127+
updateItemData(
128+
queueItem.getItemId(),
129+
mediaItem != null ? mediaItem : mediaItemConverter.toMediaItem(queueItem),
130+
mediaInfo,
131+
contentId,
132+
defaultPositionUs);
72133
}
73-
74134
return new CastTimeline(itemIds, itemIdToData);
75135
}
76136

77-
private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defaultPositionUs) {
137+
private void updateItemData(
138+
int itemId,
139+
MediaItem mediaItem,
140+
@Nullable MediaInfo mediaInfo,
141+
String contentId,
142+
long defaultPositionUs) {
78143
CastTimeline.ItemData previousData = itemIdToData.get(itemId, CastTimeline.ItemData.EMPTY);
79144
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
80145
if (durationUs == C.TIME_UNSET) {
@@ -87,7 +152,10 @@ private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defa
87152
if (defaultPositionUs == C.TIME_UNSET) {
88153
defaultPositionUs = previousData.defaultPositionUs;
89154
}
90-
itemIdToData.put(itemId, previousData.copyWithNewValues(durationUs, defaultPositionUs, isLive));
155+
itemIdToData.put(
156+
itemId,
157+
previousData.copyWithNewValues(
158+
durationUs, defaultPositionUs, isLive, mediaItem, contentId));
91159
}
92160

93161
private void removeUnusedItemDataEntries(int[] itemIds) {
@@ -99,6 +167,8 @@ private void removeUnusedItemDataEntries(int[] itemIds) {
99167
int index = 0;
100168
while (index < itemIdToData.size()) {
101169
if (!scratchItemIds.contains(itemIdToData.keyAt(index))) {
170+
CastTimeline.ItemData itemData = itemIdToData.valueAt(index);
171+
mediaItemsByContentId.remove(itemData.contentId);
102172
itemIdToData.removeAt(index);
103173
} else {
104174
index++;

0 commit comments

Comments
 (0)