1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.exoplayer2.ext.cast; 17 18 import android.util.SparseArray; 19 import androidx.annotation.Nullable; 20 import com.google.android.exoplayer2.C; 21 import com.google.android.gms.cast.MediaInfo; 22 import com.google.android.gms.cast.MediaQueueItem; 23 import com.google.android.gms.cast.MediaStatus; 24 import com.google.android.gms.cast.framework.media.RemoteMediaClient; 25 import java.util.HashSet; 26 27 /** 28 * Creates {@link CastTimeline CastTimelines} from cast receiver app status updates. 29 * 30 * <p>This class keeps track of the duration reported by the current item to fill any missing 31 * durations in the media queue items [See internal: b/65152553]. 32 */ 33 /* package */ final class CastTimelineTracker { 34 35 private final SparseArray<CastTimeline.ItemData> itemIdToData; 36 CastTimelineTracker()37 public CastTimelineTracker() { 38 itemIdToData = new SparseArray<>(); 39 } 40 41 /** 42 * Returns a {@link CastTimeline} that represents the state of the given {@code 43 * remoteMediaClient}. 44 * 45 * <p>Returned timelines may contain values obtained from {@code remoteMediaClient} in previous 46 * invocations of this method. 47 * 48 * @param remoteMediaClient The Cast media client. 49 * @return A {@link CastTimeline} that represents the given {@code remoteMediaClient} status. 50 */ getCastTimeline(RemoteMediaClient remoteMediaClient)51 public CastTimeline getCastTimeline(RemoteMediaClient remoteMediaClient) { 52 int[] itemIds = remoteMediaClient.getMediaQueue().getItemIds(); 53 if (itemIds.length > 0) { 54 // Only remove unused items when there is something in the queue to avoid removing all entries 55 // if the remote media client clears the queue temporarily. See [Internal ref: b/128825216]. 56 removeUnusedItemDataEntries(itemIds); 57 } 58 59 // TODO: Reset state when the app instance changes [Internal ref: b/129672468]. 60 MediaStatus mediaStatus = remoteMediaClient.getMediaStatus(); 61 if (mediaStatus == null) { 62 return CastTimeline.EMPTY_CAST_TIMELINE; 63 } 64 65 int currentItemId = mediaStatus.getCurrentItemId(); 66 updateItemData( 67 currentItemId, mediaStatus.getMediaInfo(), /* defaultPositionUs= */ C.TIME_UNSET); 68 69 for (MediaQueueItem item : mediaStatus.getQueueItems()) { 70 long defaultPositionUs = (long) (item.getStartTime() * C.MICROS_PER_SECOND); 71 updateItemData(item.getItemId(), item.getMedia(), defaultPositionUs); 72 } 73 74 return new CastTimeline(itemIds, itemIdToData); 75 } 76 updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defaultPositionUs)77 private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defaultPositionUs) { 78 CastTimeline.ItemData previousData = itemIdToData.get(itemId, CastTimeline.ItemData.EMPTY); 79 long durationUs = CastUtils.getStreamDurationUs(mediaInfo); 80 if (durationUs == C.TIME_UNSET) { 81 durationUs = previousData.durationUs; 82 } 83 boolean isLive = 84 mediaInfo == null 85 ? previousData.isLive 86 : mediaInfo.getStreamType() == MediaInfo.STREAM_TYPE_LIVE; 87 if (defaultPositionUs == C.TIME_UNSET) { 88 defaultPositionUs = previousData.defaultPositionUs; 89 } 90 itemIdToData.put(itemId, previousData.copyWithNewValues(durationUs, defaultPositionUs, isLive)); 91 } 92 removeUnusedItemDataEntries(int[] itemIds)93 private void removeUnusedItemDataEntries(int[] itemIds) { 94 HashSet<Integer> scratchItemIds = new HashSet<>(/* initialCapacity= */ itemIds.length * 2); 95 for (int id : itemIds) { 96 scratchItemIds.add(id); 97 } 98 99 int index = 0; 100 while (index < itemIdToData.size()) { 101 if (!scratchItemIds.contains(itemIdToData.keyAt(index))) { 102 itemIdToData.removeAt(index); 103 } else { 104 index++; 105 } 106 } 107 } 108 } 109