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:
aquilescanta 2018-01-26 08:05:52 -08:00 committed by Oliver Woodman
parent 8ba3335145
commit ac630b616c
7 changed files with 286 additions and 49 deletions

View File

@ -38,6 +38,10 @@ dependencies {
compile 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion compile 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
compile project(modulePrefix + 'library-core') compile project(modulePrefix + 'library-core')
compile project(modulePrefix + 'library-ui') 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 { ext {

View File

@ -91,6 +91,8 @@ public final class CastPlayer implements Player {
private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0]; private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0];
private final CastContext castContext; private final CastContext castContext;
// TODO: Allow custom implementations of CastTimelineTracker.
private final CastTimelineTracker timelineTracker;
private final Timeline.Window window; private final Timeline.Window window;
private final Timeline.Period period; private final Timeline.Period period;
@ -123,6 +125,7 @@ public final class CastPlayer implements Player {
*/ */
public CastPlayer(CastContext castContext) { public CastPlayer(CastContext castContext) {
this.castContext = castContext; this.castContext = castContext;
timelineTracker = new CastTimelineTracker();
window = new Timeline.Window(); window = new Timeline.Window();
period = new Timeline.Period(); period = new Timeline.Period();
statusListener = new StatusListener(); statusListener = new StatusListener();
@ -487,9 +490,11 @@ public final class CastPlayer implements Player {
@Override @Override
public long getCurrentPosition() { public long getCurrentPosition() {
return pendingSeekPositionMs != C.TIME_UNSET ? pendingSeekPositionMs return pendingSeekPositionMs != C.TIME_UNSET
: remoteMediaClient != null ? remoteMediaClient.getApproximateStreamPosition() ? pendingSeekPositionMs
: lastReportedPositionMs; : remoteMediaClient != null
? remoteMediaClient.getApproximateStreamPosition()
: lastReportedPositionMs;
} }
@Override @Override
@ -501,9 +506,9 @@ public final class CastPlayer implements Player {
public int getBufferedPercentage() { public int getBufferedPercentage() {
long position = getBufferedPosition(); long position = getBufferedPosition();
long duration = getDuration(); long duration = getDuration();
return position == C.TIME_UNSET || duration == C.TIME_UNSET ? 0 return position == C.TIME_UNSET || duration == C.TIME_UNSET
: duration == 0 ? 100 ? 0
: Util.constrainValue((int) ((position * 100) / duration), 0, 100); : duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
} }
@Override @Override
@ -598,20 +603,11 @@ public final class CastPlayer implements Player {
* Updates the current timeline and returns whether it has changed. * Updates the current timeline and returns whether it has changed.
*/ */
private boolean updateTimeline() { private boolean updateTimeline() {
MediaStatus mediaStatus = getMediaStatus(); CastTimeline oldTimeline = currentTimeline;
if (mediaStatus == null) { MediaStatus status = getMediaStatus();
boolean hasChanged = currentTimeline != CastTimeline.EMPTY_CAST_TIMELINE; currentTimeline =
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE; status != null ? timelineTracker.getCastTimeline(status) : CastTimeline.EMPTY_CAST_TIMELINE;
return hasChanged; return !oldTimeline.equals(currentTimeline);
}
List<MediaQueueItem> items = mediaStatus.getQueueItems();
if (!currentTimeline.represents(items)) {
currentTimeline = !items.isEmpty() ? new CastTimeline(mediaStatus.getQueueItems())
: CastTimeline.EMPTY_CAST_TIMELINE;
return true;
}
return false;
} }
/** /**
@ -755,10 +751,11 @@ public final class CastPlayer implements Player {
} }
private static int getRendererIndexForTrackType(int trackType) { private static int getRendererIndexForTrackType(int trackType) {
return trackType == C.TRACK_TYPE_VIDEO ? RENDERER_INDEX_VIDEO return trackType == C.TRACK_TYPE_VIDEO
: trackType == C.TRACK_TYPE_AUDIO ? RENDERER_INDEX_AUDIO ? RENDERER_INDEX_VIDEO
: trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT : trackType == C.TRACK_TYPE_AUDIO
: C.INDEX_UNSET; ? RENDERER_INDEX_AUDIO
: trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT : C.INDEX_UNSET;
} }
private static int getCastRepeatMode(@RepeatMode int repeatMode) { private static int getCastRepeatMode(@RepeatMode int repeatMode) {

View File

@ -20,8 +20,10 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
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 java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* A {@link Timeline} for Cast media queues. * A {@link Timeline} for Cast media queues.
@ -29,14 +31,19 @@ import java.util.List;
/* package */ final class CastTimeline extends Timeline { /* package */ final class CastTimeline extends Timeline {
public static final CastTimeline EMPTY_CAST_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 SparseIntArray idsToIndex;
private final int[] ids; private final int[] ids;
private final long[] durationsUs; private final long[] durationsUs;
private final long[] defaultPositionsUs; 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 itemCount = items.size();
int index = 0; int index = 0;
idsToIndex = new SparseIntArray(itemCount); idsToIndex = new SparseIntArray(itemCount);
@ -47,12 +54,19 @@ import java.util.List;
int itemId = item.getItemId(); int itemId = item.getItemId();
ids[index] = itemId; ids[index] = itemId;
idsToIndex.put(itemId, index); 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); defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
index++; index++;
} }
} }
// Timeline implementation.
@Override @Override
public int getWindowCount() { public int getWindowCount() {
return ids.length; 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; return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET;
} }
/** // equals and hashCode implementations.
* Returns whether the timeline represents a given {@code MediaQueueItem} list.
* @Override
* @param items The {@code MediaQueueItem} list. public boolean equals(Object other) {
* @return Whether the timeline represents {@code items}. if (this == other) {
*/ return true;
/* package */ boolean represents(List<MediaQueueItem> items) { } else if (!(other instanceof CastTimeline)) {
if (ids.length != items.size()) {
return false; return false;
} }
int index = 0; CastTimeline that = (CastTimeline) other;
for (MediaQueueItem item : items) { return Arrays.equals(ids, that.ids)
if (ids[index] != item.getItemId() && Arrays.equals(durationsUs, that.durationsUs)
|| durationsUs[index] != getStreamDurationUs(item.getMedia()) && Arrays.equals(defaultPositionsUs, that.defaultPositionsUs);
|| defaultPositionsUs[index] != (long) (item.getStartTime() * C.MICROS_PER_SECOND)) {
return false;
}
index++;
}
return true;
} }
private static long getStreamDurationUs(MediaInfo mediaInfo) { @Override
long durationMs = mediaInfo != null ? mediaInfo.getStreamDuration() public int hashCode() {
: MediaInfo.UNKNOWN_DURATION; int result = Arrays.hashCode(ids);
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET; result = 31 * result + Arrays.hashCode(durationsUs);
result = 31 * result + Arrays.hashCode(defaultPositionsUs);
return result;
} }
} }

View File

@ -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);
}
}

View File

@ -15,8 +15,10 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.gms.cast.CastStatusCodes; import com.google.android.gms.cast.CastStatusCodes;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaTrack; import com.google.android.gms.cast.MediaTrack;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -29,6 +31,19 @@ import java.util.Map;
private static final Map<Integer, String> CAST_STATUS_CODE_TO_STRING; 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 * Returns a descriptive log string for the given {@code statusCode}, or "Unknown." if not one of
* {@link CastStatusCodes}. * {@link CastStatusCodes}.

View File

@ -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;
}
}

View File

@ -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 * 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 * {@link Window#firstPeriodIndex} and {@link Window#lastPeriodIndex} are set correctly, and it