mirror of
https://github.com/androidx/media.git
synced 2025-05-09 16:40:55 +08:00
Propagate RemoteMediaClient's current duration to timeline if necessary
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=183390851
This commit is contained in:
parent
8ba3335145
commit
ac630b616c
@ -38,6 +38,10 @@ dependencies {
|
||||
compile 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile project(modulePrefix + 'library-ui')
|
||||
compile project(modulePrefix + 'testutils')
|
||||
testCompile 'junit:junit:' + junitVersion
|
||||
testCompile 'org.mockito:mockito-core:' + mockitoVersion
|
||||
testCompile 'org.robolectric:robolectric:' + robolectricVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -91,6 +91,8 @@ public final class CastPlayer implements Player {
|
||||
private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0];
|
||||
|
||||
private final CastContext castContext;
|
||||
// TODO: Allow custom implementations of CastTimelineTracker.
|
||||
private final CastTimelineTracker timelineTracker;
|
||||
private final Timeline.Window window;
|
||||
private final Timeline.Period period;
|
||||
|
||||
@ -123,6 +125,7 @@ public final class CastPlayer implements Player {
|
||||
*/
|
||||
public CastPlayer(CastContext castContext) {
|
||||
this.castContext = castContext;
|
||||
timelineTracker = new CastTimelineTracker();
|
||||
window = new Timeline.Window();
|
||||
period = new Timeline.Period();
|
||||
statusListener = new StatusListener();
|
||||
@ -487,8 +490,10 @@ public final class CastPlayer implements Player {
|
||||
|
||||
@Override
|
||||
public long getCurrentPosition() {
|
||||
return pendingSeekPositionMs != C.TIME_UNSET ? pendingSeekPositionMs
|
||||
: remoteMediaClient != null ? remoteMediaClient.getApproximateStreamPosition()
|
||||
return pendingSeekPositionMs != C.TIME_UNSET
|
||||
? pendingSeekPositionMs
|
||||
: remoteMediaClient != null
|
||||
? remoteMediaClient.getApproximateStreamPosition()
|
||||
: lastReportedPositionMs;
|
||||
}
|
||||
|
||||
@ -501,9 +506,9 @@ public final class CastPlayer implements Player {
|
||||
public int getBufferedPercentage() {
|
||||
long position = getBufferedPosition();
|
||||
long duration = getDuration();
|
||||
return position == C.TIME_UNSET || duration == C.TIME_UNSET ? 0
|
||||
: duration == 0 ? 100
|
||||
: Util.constrainValue((int) ((position * 100) / duration), 0, 100);
|
||||
return position == C.TIME_UNSET || duration == C.TIME_UNSET
|
||||
? 0
|
||||
: duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -598,20 +603,11 @@ public final class CastPlayer implements Player {
|
||||
* Updates the current timeline and returns whether it has changed.
|
||||
*/
|
||||
private boolean updateTimeline() {
|
||||
MediaStatus mediaStatus = getMediaStatus();
|
||||
if (mediaStatus == null) {
|
||||
boolean hasChanged = currentTimeline != CastTimeline.EMPTY_CAST_TIMELINE;
|
||||
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
|
||||
return hasChanged;
|
||||
}
|
||||
|
||||
List<MediaQueueItem> items = mediaStatus.getQueueItems();
|
||||
if (!currentTimeline.represents(items)) {
|
||||
currentTimeline = !items.isEmpty() ? new CastTimeline(mediaStatus.getQueueItems())
|
||||
: CastTimeline.EMPTY_CAST_TIMELINE;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
CastTimeline oldTimeline = currentTimeline;
|
||||
MediaStatus status = getMediaStatus();
|
||||
currentTimeline =
|
||||
status != null ? timelineTracker.getCastTimeline(status) : CastTimeline.EMPTY_CAST_TIMELINE;
|
||||
return !oldTimeline.equals(currentTimeline);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -755,10 +751,11 @@ public final class CastPlayer implements Player {
|
||||
}
|
||||
|
||||
private static int getRendererIndexForTrackType(int trackType) {
|
||||
return trackType == C.TRACK_TYPE_VIDEO ? RENDERER_INDEX_VIDEO
|
||||
: trackType == C.TRACK_TYPE_AUDIO ? RENDERER_INDEX_AUDIO
|
||||
: trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT
|
||||
: C.INDEX_UNSET;
|
||||
return trackType == C.TRACK_TYPE_VIDEO
|
||||
? RENDERER_INDEX_VIDEO
|
||||
: trackType == C.TRACK_TYPE_AUDIO
|
||||
? RENDERER_INDEX_AUDIO
|
||||
: trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
private static int getCastRepeatMode(@RepeatMode int repeatMode) {
|
||||
|
@ -20,8 +20,10 @@ import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link Timeline} for Cast media queues.
|
||||
@ -29,14 +31,19 @@ import java.util.List;
|
||||
/* package */ final class CastTimeline extends Timeline {
|
||||
|
||||
public static final CastTimeline EMPTY_CAST_TIMELINE =
|
||||
new CastTimeline(Collections.<MediaQueueItem>emptyList());
|
||||
new CastTimeline(
|
||||
Collections.<MediaQueueItem>emptyList(), Collections.<String, Long>emptyMap());
|
||||
|
||||
private final SparseIntArray idsToIndex;
|
||||
private final int[] ids;
|
||||
private final long[] durationsUs;
|
||||
private final long[] defaultPositionsUs;
|
||||
|
||||
public CastTimeline(List<MediaQueueItem> items) {
|
||||
/**
|
||||
* @param items A list of cast media queue items to represent.
|
||||
* @param contentIdToDurationUsMap A map of content id to duration in microseconds.
|
||||
*/
|
||||
public CastTimeline(List<MediaQueueItem> items, Map<String, Long> contentIdToDurationUsMap) {
|
||||
int itemCount = items.size();
|
||||
int index = 0;
|
||||
idsToIndex = new SparseIntArray(itemCount);
|
||||
@ -47,12 +54,19 @@ import java.util.List;
|
||||
int itemId = item.getItemId();
|
||||
ids[index] = itemId;
|
||||
idsToIndex.put(itemId, index);
|
||||
durationsUs[index] = getStreamDurationUs(item.getMedia());
|
||||
MediaInfo mediaInfo = item.getMedia();
|
||||
String contentId = mediaInfo.getContentId();
|
||||
durationsUs[index] =
|
||||
contentIdToDurationUsMap.containsKey(contentId)
|
||||
? contentIdToDurationUsMap.get(contentId)
|
||||
: CastUtils.getStreamDurationUs(mediaInfo);
|
||||
defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
// Timeline implementation.
|
||||
|
||||
@Override
|
||||
public int getWindowCount() {
|
||||
return ids.length;
|
||||
@ -83,32 +97,27 @@ import java.util.List;
|
||||
return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the timeline represents a given {@code MediaQueueItem} list.
|
||||
*
|
||||
* @param items The {@code MediaQueueItem} list.
|
||||
* @return Whether the timeline represents {@code items}.
|
||||
*/
|
||||
/* package */ boolean represents(List<MediaQueueItem> items) {
|
||||
if (ids.length != items.size()) {
|
||||
return false;
|
||||
}
|
||||
int index = 0;
|
||||
for (MediaQueueItem item : items) {
|
||||
if (ids[index] != item.getItemId()
|
||||
|| durationsUs[index] != getStreamDurationUs(item.getMedia())
|
||||
|| defaultPositionsUs[index] != (long) (item.getStartTime() * C.MICROS_PER_SECOND)) {
|
||||
return false;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
// equals and hashCode implementations.
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
} else if (!(other instanceof CastTimeline)) {
|
||||
return false;
|
||||
}
|
||||
CastTimeline that = (CastTimeline) other;
|
||||
return Arrays.equals(ids, that.ids)
|
||||
&& Arrays.equals(durationsUs, that.durationsUs)
|
||||
&& Arrays.equals(defaultPositionsUs, that.defaultPositionsUs);
|
||||
}
|
||||
|
||||
private static long getStreamDurationUs(MediaInfo mediaInfo) {
|
||||
long durationMs = mediaInfo != null ? mediaInfo.getStreamDuration()
|
||||
: MediaInfo.UNKNOWN_DURATION;
|
||||
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Arrays.hashCode(ids);
|
||||
result = 31 * result + Arrays.hashCode(durationsUs);
|
||||
result = 31 * result + Arrays.hashCode(defaultPositionsUs);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Creates {@link CastTimeline}s from cast receiver app media status.
|
||||
*
|
||||
* <p>This class keeps track of the duration reported by the current item to fill any missing
|
||||
* durations in the media queue items [See internal: b/65152553].
|
||||
*/
|
||||
/* package */ final class CastTimelineTracker {
|
||||
|
||||
private final HashMap<String, Long> contentIdToDurationUsMap;
|
||||
private final HashSet<String> scratchContentIdSet;
|
||||
|
||||
public CastTimelineTracker() {
|
||||
contentIdToDurationUsMap = new HashMap<>();
|
||||
scratchContentIdSet = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link CastTimeline} that represent the given {@code status}.
|
||||
*
|
||||
* @param status The Cast media status.
|
||||
* @return A {@link CastTimeline} that represent the given {@code status}.
|
||||
*/
|
||||
public CastTimeline getCastTimeline(MediaStatus status) {
|
||||
MediaInfo mediaInfo = status.getMediaInfo();
|
||||
List<MediaQueueItem> items = status.getQueueItems();
|
||||
removeUnusedDurationEntries(items);
|
||||
|
||||
if (mediaInfo != null) {
|
||||
String contentId = mediaInfo.getContentId();
|
||||
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
|
||||
contentIdToDurationUsMap.put(contentId, durationUs);
|
||||
}
|
||||
return new CastTimeline(items, contentIdToDurationUsMap);
|
||||
}
|
||||
|
||||
private void removeUnusedDurationEntries(List<MediaQueueItem> items) {
|
||||
scratchContentIdSet.clear();
|
||||
for (MediaQueueItem item : items) {
|
||||
scratchContentIdSet.add(item.getMedia().getContentId());
|
||||
}
|
||||
contentIdToDurationUsMap.keySet().retainAll(scratchContentIdSet);
|
||||
}
|
||||
}
|
@ -15,8 +15,10 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.gms.cast.CastStatusCodes;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaTrack;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@ -29,6 +31,19 @@ import java.util.Map;
|
||||
|
||||
private static final Map<Integer, String> CAST_STATUS_CODE_TO_STRING;
|
||||
|
||||
/**
|
||||
* Returns the duration in microseconds advertised by a media info, or {@link C#TIME_UNSET} if
|
||||
* unknown or not applicable.
|
||||
*
|
||||
* @param mediaInfo The media info to get the duration from.
|
||||
* @return The duration in microseconds.
|
||||
*/
|
||||
public static long getStreamDurationUs(MediaInfo mediaInfo) {
|
||||
long durationMs =
|
||||
mediaInfo != null ? mediaInfo.getStreamDuration() : MediaInfo.UNKNOWN_DURATION;
|
||||
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a descriptive log string for the given {@code statusCode}, or "Unknown." if not one of
|
||||
* {@link CastStatusCodes}.
|
||||
|
@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
import java.util.ArrayList;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
/** Tests for {@link CastTimelineTracker}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE)
|
||||
public class CastTimelineTrackerTest {
|
||||
|
||||
private static final long DURATION_1_MS = 1000;
|
||||
private static final long DURATION_2_MS = 2000;
|
||||
private static final long DURATION_3_MS = 3000;
|
||||
private static final long DURATION_4_MS = 4000;
|
||||
private static final long DURATION_5_MS = 5000;
|
||||
|
||||
/** Tests that duration of the current media info is correctly propagated to the timeline. */
|
||||
@Test
|
||||
public void testGetCastTimeline() {
|
||||
MediaInfo mediaInfo;
|
||||
MediaStatus status =
|
||||
mockMediaStatus(
|
||||
new int[] {1, 2, 3},
|
||||
new String[] {"contentId1", "contentId2", "contentId3"},
|
||||
new long[] {DURATION_1_MS, MediaInfo.UNKNOWN_DURATION, MediaInfo.UNKNOWN_DURATION});
|
||||
|
||||
CastTimelineTracker tracker = new CastTimelineTracker();
|
||||
mediaInfo = mockMediaInfo("contentId1", DURATION_1_MS);
|
||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(status), C.msToUs(DURATION_1_MS), C.TIME_UNSET, C.TIME_UNSET);
|
||||
|
||||
mediaInfo = mockMediaInfo("contentId3", DURATION_3_MS);
|
||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(status),
|
||||
C.msToUs(DURATION_1_MS),
|
||||
C.TIME_UNSET,
|
||||
C.msToUs(DURATION_3_MS));
|
||||
|
||||
mediaInfo = mockMediaInfo("contentId2", DURATION_2_MS);
|
||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(status),
|
||||
C.msToUs(DURATION_1_MS),
|
||||
C.msToUs(DURATION_2_MS),
|
||||
C.msToUs(DURATION_3_MS));
|
||||
|
||||
MediaStatus newStatus =
|
||||
mockMediaStatus(
|
||||
new int[] {4, 1, 5, 3},
|
||||
new String[] {"contentId4", "contentId1", "contentId5", "contentId3"},
|
||||
new long[] {
|
||||
MediaInfo.UNKNOWN_DURATION,
|
||||
MediaInfo.UNKNOWN_DURATION,
|
||||
DURATION_5_MS,
|
||||
MediaInfo.UNKNOWN_DURATION
|
||||
});
|
||||
mediaInfo = mockMediaInfo("contentId5", DURATION_5_MS);
|
||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(newStatus),
|
||||
C.TIME_UNSET,
|
||||
C.msToUs(DURATION_1_MS),
|
||||
C.msToUs(DURATION_5_MS),
|
||||
C.msToUs(DURATION_3_MS));
|
||||
|
||||
mediaInfo = mockMediaInfo("contentId3", DURATION_3_MS);
|
||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(newStatus),
|
||||
C.TIME_UNSET,
|
||||
C.msToUs(DURATION_1_MS),
|
||||
C.msToUs(DURATION_5_MS),
|
||||
C.msToUs(DURATION_3_MS));
|
||||
|
||||
mediaInfo = mockMediaInfo("contentId4", DURATION_4_MS);
|
||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
||||
TimelineAsserts.assertPeriodDurations(
|
||||
tracker.getCastTimeline(newStatus),
|
||||
C.msToUs(DURATION_4_MS),
|
||||
C.msToUs(DURATION_1_MS),
|
||||
C.msToUs(DURATION_5_MS),
|
||||
C.msToUs(DURATION_3_MS));
|
||||
}
|
||||
|
||||
private static MediaStatus mockMediaStatus(
|
||||
int[] itemIds, String[] contentIds, long[] durationsMs) {
|
||||
ArrayList<MediaQueueItem> items = new ArrayList<>();
|
||||
for (int i = 0; i < contentIds.length; i++) {
|
||||
MediaInfo mediaInfo = mockMediaInfo(contentIds[i], durationsMs[i]);
|
||||
MediaQueueItem item = Mockito.mock(MediaQueueItem.class);
|
||||
Mockito.when(item.getMedia()).thenReturn(mediaInfo);
|
||||
Mockito.when(item.getItemId()).thenReturn(itemIds[i]);
|
||||
items.add(item);
|
||||
}
|
||||
MediaStatus status = Mockito.mock(MediaStatus.class);
|
||||
Mockito.when(status.getQueueItems()).thenReturn(items);
|
||||
return status;
|
||||
}
|
||||
|
||||
private static MediaInfo mockMediaInfo(String contentId, long durationMs) {
|
||||
MediaInfo mediaInfo = Mockito.mock(MediaInfo.class);
|
||||
Mockito.when(mediaInfo.getContentId()).thenReturn(contentId);
|
||||
Mockito.when(mediaInfo.getStreamDuration()).thenReturn(durationMs);
|
||||
return mediaInfo;
|
||||
}
|
||||
}
|
@ -99,6 +99,19 @@ public final class TimelineAsserts {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the durations of the periods in the {@link Timeline} and the durations in the
|
||||
* given sequence are equal.
|
||||
*/
|
||||
public static void assertPeriodDurations(Timeline timeline, long... durationsUs) {
|
||||
int periodCount = timeline.getPeriodCount();
|
||||
assertThat(periodCount).isEqualTo(durationsUs.length);
|
||||
Period period = new Period();
|
||||
for (int i = 0; i < periodCount; i++) {
|
||||
assertThat(timeline.getPeriod(i, period).durationUs).isEqualTo(durationsUs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that period counts for each window are set correctly. Also asserts that
|
||||
* {@link Window#firstPeriodIndex} and {@link Window#lastPeriodIndex} are set correctly, and it
|
||||
|
Loading…
x
Reference in New Issue
Block a user