Skip to content

Commit

Permalink
Use the public MediaItem in the timeline of CastPlayer
Browse files Browse the repository at this point in the history
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
  • Loading branch information
marcbaechinger authored and rohitjoins committed Jul 12, 2022
1 parent 7dc54ef commit 30fbc3a
Show file tree
Hide file tree
Showing 6 changed files with 436 additions and 133 deletions.
5 changes: 5 additions & 0 deletions RELEASENOTES.md
Expand Up @@ -35,6 +35,11 @@
* Leanback extension:
* Listen to `playWhenReady` changes in `LeanbackAdapter`
([10420](https://github.com/google/ExoPlayer/issues/10420)).
* Cast:
* Use the `MediaItem` that has been passed to the playlist methods as
`Window.mediaItem` in `CastTimeline`
([#25](https://github.com/androidx/media/issues/25),
[#8212](https://github.com/google/ExoPlayer/issues/8212)).

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

Expand Down
39 changes: 19 additions & 20 deletions libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java
Expand Up @@ -200,7 +200,7 @@ public CastPlayer(
this.mediaItemConverter = mediaItemConverter;
this.seekBackIncrementMs = seekBackIncrementMs;
this.seekForwardIncrementMs = seekForwardIncrementMs;
timelineTracker = new CastTimelineTracker();
timelineTracker = new CastTimelineTracker(mediaItemConverter);
period = new Timeline.Period();
statusListener = new StatusListener();
seekResultCallback = new SeekResultCallback();
Expand Down Expand Up @@ -285,8 +285,7 @@ public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {

@Override
public void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
setMediaItemsInternal(
toMediaQueueItems(mediaItems), startIndex, startPositionMs, repeatMode.value);
setMediaItemsInternal(mediaItems, startIndex, startPositionMs, repeatMode.value);
}

@Override
Expand All @@ -296,7 +295,7 @@ public void addMediaItems(int index, List<MediaItem> mediaItems) {
if (index < currentTimeline.getWindowCount()) {
uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid;
}
addMediaItemsInternal(toMediaQueueItems(mediaItems), uid);
addMediaItemsInternal(mediaItems, uid);
}

@Override
Expand Down Expand Up @@ -1022,14 +1021,13 @@ private void updateAvailableCommandsAndNotifyIfChanged() {
}
}

@Nullable
private PendingResult<MediaChannelResult> setMediaItemsInternal(
MediaQueueItem[] mediaQueueItems,
private void setMediaItemsInternal(
List<MediaItem> mediaItems,
int startIndex,
long startPositionMs,
@RepeatMode int repeatMode) {
if (remoteMediaClient == null || mediaQueueItems.length == 0) {
return null;
if (remoteMediaClient == null || mediaItems.isEmpty()) {
return;
}
startPositionMs = startPositionMs == C.TIME_UNSET ? 0 : startPositionMs;
if (startIndex == C.INDEX_UNSET) {
Expand All @@ -1040,34 +1038,35 @@ private PendingResult<MediaChannelResult> setMediaItemsInternal(
if (!currentTimeline.isEmpty()) {
pendingMediaItemRemovalPosition = getCurrentPositionInfo();
}
return remoteMediaClient.queueLoad(
MediaQueueItem[] mediaQueueItems = toMediaQueueItems(mediaItems);
timelineTracker.onMediaItemsSet(mediaItems, mediaQueueItems);
remoteMediaClient.queueLoad(
mediaQueueItems,
min(startIndex, mediaQueueItems.length - 1),
min(startIndex, mediaItems.size() - 1),
getCastRepeatMode(repeatMode),
startPositionMs,
/* customData= */ null);
}

@Nullable
private PendingResult<MediaChannelResult> addMediaItemsInternal(MediaQueueItem[] items, int uid) {
private void addMediaItemsInternal(List<MediaItem> mediaItems, int uid) {
if (remoteMediaClient == null || getMediaStatus() == null) {
return null;
return;
}
return remoteMediaClient.queueInsertItems(items, uid, /* customData= */ null);
MediaQueueItem[] itemsToInsert = toMediaQueueItems(mediaItems);
timelineTracker.onMediaItemsAdded(mediaItems, itemsToInsert);
remoteMediaClient.queueInsertItems(itemsToInsert, uid, /* customData= */ null);
}

@Nullable
private PendingResult<MediaChannelResult> moveMediaItemsInternal(
int[] uids, int fromIndex, int newIndex) {
private void moveMediaItemsInternal(int[] uids, int fromIndex, int newIndex) {
if (remoteMediaClient == null || getMediaStatus() == null) {
return null;
return;
}
int insertBeforeIndex = fromIndex < newIndex ? newIndex + uids.length : newIndex;
int insertBeforeItemId = MediaQueueItem.INVALID_ITEM_ID;
if (insertBeforeIndex < currentTimeline.getWindowCount()) {
insertBeforeItemId = (int) currentTimeline.getWindow(insertBeforeIndex, window).uid;
}
return remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null);
remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null);
}

@Nullable
Expand Down
47 changes: 37 additions & 10 deletions libraries/cast/src/main/java/androidx/media3/cast/CastTimeline.java
Expand Up @@ -15,13 +15,13 @@
*/
package androidx.media3.cast;

import android.net.Uri;
import android.util.SparseArray;
import android.util.SparseIntArray;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import com.google.android.gms.cast.MediaInfo;
import java.util.Arrays;

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

/* package */ static final String UNKNOWN_CONTENT_ID = "UNKNOWN_CONTENT_ID";

/** Holds no media information. */
public static final ItemData EMPTY =
new ItemData(
/* durationUs= */ C.TIME_UNSET,
/* defaultPositionUs= */ C.TIME_UNSET,
/* isLive= */ false);
/* isLive= */ false,
MediaItem.EMPTY,
UNKNOWN_CONTENT_ID);

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

/**
* Creates an instance.
*
* @param durationUs See {@link #durationsUs}.
* @param defaultPositionUs See {@link #defaultPositionUs}.
* @param isLive See {@link #isLive}.
* @param mediaItem See {@link #mediaItem}.
* @param contentId See {@link #contentId}.
*/
public ItemData(long durationUs, long defaultPositionUs, boolean isLive) {
public ItemData(
long durationUs,
long defaultPositionUs,
boolean isLive,
MediaItem mediaItem,
String contentId) {
this.durationUs = durationUs;
this.defaultPositionUs = defaultPositionUs;
this.isLive = isLive;
this.mediaItem = mediaItem;
this.contentId = contentId;
}

/**
Expand All @@ -66,14 +83,23 @@ public ItemData(long durationUs, long defaultPositionUs, boolean isLive) {
* @param defaultPositionUs The default start position in microseconds, or {@link C#TIME_UNSET}
* if unknown.
* @param isLive Whether the item is live, or {@code false} if unknown.
* @param mediaItem The media item.
* @param contentId The content ID.
*/
public ItemData copyWithNewValues(long durationUs, long defaultPositionUs, boolean isLive) {
public ItemData copyWithNewValues(
long durationUs,
long defaultPositionUs,
boolean isLive,
MediaItem mediaItem,
String contentId) {
if (durationUs == this.durationUs
&& defaultPositionUs == this.defaultPositionUs
&& isLive == this.isLive) {
&& isLive == this.isLive
&& contentId.equals(this.contentId)
&& mediaItem.equals(this.mediaItem)) {
return this;
}
return new ItemData(durationUs, defaultPositionUs, isLive);
return new ItemData(durationUs, defaultPositionUs, isLive, mediaItem, contentId);
}
}

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

private final SparseIntArray idsToIndex;
private final MediaItem[] mediaItems;
private final int[] ids;
private final long[] durationsUs;
private final long[] defaultPositionsUs;
Expand All @@ -100,10 +127,12 @@ public CastTimeline(int[] itemIds, SparseArray<ItemData> itemIdToData) {
durationsUs = new long[itemCount];
defaultPositionsUs = new long[itemCount];
isLive = new boolean[itemCount];
mediaItems = new MediaItem[itemCount];
for (int i = 0; i < ids.length; i++) {
int id = ids[i];
idsToIndex.put(id, i);
ItemData data = itemIdToData.get(id, ItemData.EMPTY);
mediaItems[i] = data.mediaItem.buildUpon().setTag(id).build();
durationsUs[i] = data.durationUs;
defaultPositionsUs[i] = data.defaultPositionUs == C.TIME_UNSET ? 0 : data.defaultPositionUs;
isLive[i] = data.isLive;
Expand All @@ -121,18 +150,16 @@ public int getWindowCount() {
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
long durationUs = durationsUs[windowIndex];
boolean isDynamic = durationUs == C.TIME_UNSET;
MediaItem mediaItem =
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build();
return window.set(
/* uid= */ ids[windowIndex],
/* mediaItem= */ mediaItem,
/* mediaItem= */ mediaItems[windowIndex],
/* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* isSeekable= */ !isDynamic,
isDynamic,
isLive[windowIndex] ? mediaItem.liveConfiguration : null,
isLive[windowIndex] ? mediaItems[windowIndex].liveConfiguration : null,
defaultPositionsUs[windowIndex],
durationUs,
/* firstPeriodIndex= */ windowIndex,
Expand Down
Expand Up @@ -15,14 +15,23 @@
*/
package androidx.media3.cast;

import static androidx.media3.cast.CastTimeline.ItemData.UNKNOWN_CONTENT_ID;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;

import android.util.SparseArray;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

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

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

public CastTimelineTracker() {
/**
* Creates an instance.
*
* @param mediaItemConverter The converter used to convert from a {@link MediaQueueItem} to a
* {@link MediaItem}.
*/
public CastTimelineTracker(MediaItemConverter mediaItemConverter) {
this.mediaItemConverter = mediaItemConverter;
itemIdToData = new SparseArray<>();
mediaItemsByContentId = new HashMap<>();
}

/**
* Called when media items {@linkplain Player#setMediaItems have been set to the playlist} and are
* sent to the cast playback queue. A future queue update of the {@link RemoteMediaClient} will
* reflect this addition.
*
* @param mediaItems The media items that have been set.
* @param mediaQueueItems The corresponding media queue items.
*/
public void onMediaItemsSet(List<MediaItem> mediaItems, MediaQueueItem[] mediaQueueItems) {
mediaItemsByContentId.clear();
onMediaItemsAdded(mediaItems, mediaQueueItems);
}

/**
* Called when media items {@linkplain Player#addMediaItems(List) have been added} and are sent to
* the cast playback queue. A future queue update of the {@link RemoteMediaClient} will reflect
* this addition.
*
* @param mediaItems The media items that have been added.
* @param mediaQueueItems The corresponding media queue items.
*/
public void onMediaItemsAdded(List<MediaItem> mediaItems, MediaQueueItem[] mediaQueueItems) {
for (int i = 0; i < mediaItems.size(); i++) {
mediaItemsByContentId.put(
checkNotNull(mediaQueueItems[i].getMedia()).getContentId(), mediaItems.get(i));
}
}

/**
Expand Down Expand Up @@ -63,18 +110,36 @@ public CastTimeline getCastTimeline(RemoteMediaClient remoteMediaClient) {
}

int currentItemId = mediaStatus.getCurrentItemId();
String currentContentId = checkStateNotNull(mediaStatus.getMediaInfo()).getContentId();
MediaItem mediaItem = mediaItemsByContentId.get(currentContentId);
updateItemData(
currentItemId, mediaStatus.getMediaInfo(), /* defaultPositionUs= */ C.TIME_UNSET);
currentItemId,
mediaItem != null ? mediaItem : MediaItem.EMPTY,
mediaStatus.getMediaInfo(),
currentContentId,
/* defaultPositionUs= */ C.TIME_UNSET);

for (MediaQueueItem item : mediaStatus.getQueueItems()) {
long defaultPositionUs = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
updateItemData(item.getItemId(), item.getMedia(), defaultPositionUs);
for (MediaQueueItem queueItem : mediaStatus.getQueueItems()) {
long defaultPositionUs = (long) (queueItem.getStartTime() * C.MICROS_PER_SECOND);
@Nullable MediaInfo mediaInfo = queueItem.getMedia();
String contentId = mediaInfo != null ? mediaInfo.getContentId() : UNKNOWN_CONTENT_ID;
mediaItem = mediaItemsByContentId.get(contentId);
updateItemData(
queueItem.getItemId(),
mediaItem != null ? mediaItem : mediaItemConverter.toMediaItem(queueItem),
mediaInfo,
contentId,
defaultPositionUs);
}

return new CastTimeline(itemIds, itemIdToData);
}

private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defaultPositionUs) {
private void updateItemData(
int itemId,
MediaItem mediaItem,
@Nullable MediaInfo mediaInfo,
String contentId,
long defaultPositionUs) {
CastTimeline.ItemData previousData = itemIdToData.get(itemId, CastTimeline.ItemData.EMPTY);
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
if (durationUs == C.TIME_UNSET) {
Expand All @@ -87,7 +152,10 @@ private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defa
if (defaultPositionUs == C.TIME_UNSET) {
defaultPositionUs = previousData.defaultPositionUs;
}
itemIdToData.put(itemId, previousData.copyWithNewValues(durationUs, defaultPositionUs, isLive));
itemIdToData.put(
itemId,
previousData.copyWithNewValues(
durationUs, defaultPositionUs, isLive, mediaItem, contentId));
}

private void removeUnusedItemDataEntries(int[] itemIds) {
Expand All @@ -99,6 +167,8 @@ private void removeUnusedItemDataEntries(int[] itemIds) {
int index = 0;
while (index < itemIdToData.size()) {
if (!scratchItemIds.contains(itemIdToData.keyAt(index))) {
CastTimeline.ItemData itemData = itemIdToData.valueAt(index);
mediaItemsByContentId.remove(itemData.contentId);
itemIdToData.removeAt(index);
} else {
index++;
Expand Down

0 comments on commit 30fbc3a

Please sign in to comment.