Add media queue support to CastPlayer

Also workaround the non-repeatable queue and fix other minor issues.

Issue:#2283

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=166848894
This commit is contained in:
aquilescanta 2017-08-29 08:16:11 -07:00 committed by Oliver Woodman
parent f44e30c754
commit aafdd2267a
6 changed files with 494 additions and 187 deletions

View File

@ -52,17 +52,17 @@ import java.util.List;
/** /**
* The mime type of the media sample, as required by {@link MediaInfo#setContentType}. * The mime type of the media sample, as required by {@link MediaInfo#setContentType}.
*/ */
public final String type; public final String mimeType;
/** /**
* @param uri See {@link #uri}. * @param uri See {@link #uri}.
* @param name See {@link #name}. * @param name See {@link #name}.
* @param type See {@link #type}. * @param mimeType See {@link #mimeType}.
*/ */
public Sample(String uri, String name, String type) { public Sample(String uri, String name, String mimeType) {
this.uri = uri; this.uri = uri;
this.name = name; this.name = name;
this.type = type; this.mimeType = mimeType;
} }
@Override @Override

View File

@ -37,6 +37,9 @@ import com.google.android.exoplayer2.ui.PlaybackControlView;
import com.google.android.exoplayer2.ui.SimpleExoPlayerView; import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.cast.framework.CastContext;
/** /**
@ -95,12 +98,12 @@ import com.google.android.gms.cast.framework.CastContext;
boolean playWhenReady) { boolean playWhenReady) {
this.currentSample = currentSample; this.currentSample = currentSample;
if (playbackLocation == PLAYBACK_REMOTE) { if (playbackLocation == PLAYBACK_REMOTE) {
castPlayer.load(currentSample.name, currentSample.uri, currentSample.type, positionMs, castPlayer.loadItem(buildMediaQueueItem(currentSample), positionMs);
playWhenReady); castPlayer.setPlayWhenReady(playWhenReady);
} else /* playbackLocation == PLAYBACK_LOCAL */ { } else /* playbackLocation == PLAYBACK_LOCAL */ {
exoPlayer.prepare(buildMediaSource(currentSample), true, true);
exoPlayer.setPlayWhenReady(playWhenReady); exoPlayer.setPlayWhenReady(playWhenReady);
exoPlayer.seekTo(positionMs); exoPlayer.seekTo(positionMs);
exoPlayer.prepare(buildMediaSource(currentSample), true, true);
} }
} }
@ -143,9 +146,18 @@ import com.google.android.gms.cast.framework.CastContext;
// Internal methods. // Internal methods.
private static MediaQueueItem buildMediaQueueItem(CastDemoUtil.Sample sample) {
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name);
MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType)
.setMetadata(movieMetadata).build();
return new MediaQueueItem.Builder(mediaInfo).build();
}
private static MediaSource buildMediaSource(CastDemoUtil.Sample sample) { private static MediaSource buildMediaSource(CastDemoUtil.Sample sample) {
Uri uri = Uri.parse(sample.uri); Uri uri = Uri.parse(sample.uri);
switch (sample.type) { switch (sample.mimeType) {
case CastDemoUtil.MIME_TYPE_SS: case CastDemoUtil.MIME_TYPE_SS:
return new SsMediaSource(uri, DATA_SOURCE_FACTORY, return new SsMediaSource(uri, DATA_SOURCE_FACTORY,
new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), null, null); new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), null, null);
@ -158,7 +170,7 @@ import com.google.android.gms.cast.framework.CastContext;
return new ExtractorMediaSource(uri, DATA_SOURCE_FACTORY, new DefaultExtractorsFactory(), return new ExtractorMediaSource(uri, DATA_SOURCE_FACTORY, new DefaultExtractorsFactory(),
null, null); null, null);
default: { default: {
throw new IllegalStateException("Unsupported type: " + sample.type); throw new IllegalStateException("Unsupported type: " + sample.mimeType);
} }
} }
} }
@ -177,14 +189,16 @@ import com.google.android.gms.cast.framework.CastContext;
castControlView.show(); castControlView.show();
} }
long playbackPositionMs = 0; long playbackPositionMs;
boolean playWhenReady = true; boolean playWhenReady;
if (exoPlayer != null) { if (this.playbackLocation == PLAYBACK_LOCAL) {
playbackPositionMs = exoPlayer.getCurrentPosition(); playbackPositionMs = exoPlayer.getCurrentPosition();
playWhenReady = exoPlayer.getPlayWhenReady(); playWhenReady = exoPlayer.getPlayWhenReady();
} else if (this.playbackLocation == PLAYBACK_REMOTE) { exoPlayer.stop();
} else /* this.playbackLocation == PLAYBACK_REMOTE */ {
playbackPositionMs = castPlayer.getCurrentPosition(); playbackPositionMs = castPlayer.getCurrentPosition();
playWhenReady = castPlayer.getPlayWhenReady(); playWhenReady = castPlayer.getPlayWhenReady();
castPlayer.stop();
} }
this.playbackLocation = playbackLocation; this.playbackLocation = playbackLocation;

View File

@ -35,7 +35,8 @@
android:id="@+id/cast_control_view" android:id="@+id/cast_control_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:show_timeout="-1"
android:layout_weight="2" android:layout_weight="2"
android:visibility="gone"/> android:visibility="gone"
app:repeat_toggle_modes="all|one"
app:show_timeout="-1"/>
</LinearLayout> </LinearLayout>

View File

@ -15,23 +15,24 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.FixedTrackSelection; import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
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.MediaInfo;
import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.MediaTrack; import com.google.android.gms.cast.MediaTrack;
import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.cast.framework.CastContext;
@ -41,6 +42,7 @@ import com.google.android.gms.cast.framework.SessionManagerListener;
import com.google.android.gms.cast.framework.media.RemoteMediaClient; import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult; import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.ResultCallback;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
@ -48,19 +50,16 @@ import java.util.concurrent.CopyOnWriteArraySet;
/** /**
* {@link Player} implementation that communicates with a Cast receiver app. * {@link Player} implementation that communicates with a Cast receiver app.
* *
* <p>Calls to the methods in this class depend on the availability of an underlying cast session. * <p>The behavior of this class depends on the underlying Cast session, which is obtained from the
* If no session is available, method calls have no effect. To keep track of the underyling session, * Cast context passed to {@link #CastPlayer}. To keep track of the session,
* {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be * {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
* implemented and attached to the player. * implemented and attached to the player.</p>
* *
* <p>Methods should be called on the application's main thread. * <p> If no session is available, the player state will remain unchanged and calls to methods that
* alter it will be ignored. Querying the player state is possible even when no session is
* available, in which case, the last observed receiver app state is reported.</p>
* *
* <p>Known issues: * <p>Methods should be called on the application's main thread.</p>
* <ul>
* <li>Part of the Cast API is not exposed through this interface. For instance, volume settings
* and track selection.</li>
* <li> Repeat mode is not working. See [internal: b/64137174].</li>
* </ul>
*/ */
public final class CastPlayer implements Player { public final class CastPlayer implements Player {
@ -95,10 +94,12 @@ public final class CastPlayer implements Player {
private final CastContext castContext; private final CastContext castContext;
private final Timeline.Window window; private final Timeline.Window window;
private final Timeline.Period period;
private RemoteMediaClient remoteMediaClient;
// Result callbacks. // Result callbacks.
private final StatusListener statusListener; private final StatusListener statusListener;
private final RepeatModeResultCallback repeatModeResultCallback;
private final SeekResultCallback seekResultCallback; private final SeekResultCallback seekResultCallback;
// Listeners. // Listeners.
@ -106,11 +107,15 @@ public final class CastPlayer implements Player {
private SessionAvailabilityListener sessionAvailabilityListener; private SessionAvailabilityListener sessionAvailabilityListener;
// Internal state. // Internal state.
private RemoteMediaClient remoteMediaClient; private CastTimeline currentTimeline;
private Timeline currentTimeline;
private TrackGroupArray currentTrackGroups; private TrackGroupArray currentTrackGroups;
private TrackSelectionArray currentTrackSelection; private TrackSelectionArray currentTrackSelection;
private int playbackState;
private int repeatMode;
private int currentWindowIndex;
private boolean playWhenReady;
private long lastReportedPositionMs; private long lastReportedPositionMs;
private int pendingSeekWindowIndex;
private long pendingSeekPositionMs; private long pendingSeekPositionMs;
/** /**
@ -119,41 +124,142 @@ public final class CastPlayer implements Player {
public CastPlayer(CastContext castContext) { public CastPlayer(CastContext castContext) {
this.castContext = castContext; this.castContext = castContext;
window = new Timeline.Window(); window = new Timeline.Window();
period = new Timeline.Period();
statusListener = new StatusListener(); statusListener = new StatusListener();
repeatModeResultCallback = new RepeatModeResultCallback();
seekResultCallback = new SeekResultCallback(); seekResultCallback = new SeekResultCallback();
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArraySet<>();
SessionManager sessionManager = castContext.getSessionManager(); SessionManager sessionManager = castContext.getSessionManager();
sessionManager.addSessionManagerListener(statusListener, CastSession.class); sessionManager.addSessionManagerListener(statusListener, CastSession.class);
CastSession session = sessionManager.getCurrentCastSession(); CastSession session = sessionManager.getCurrentCastSession();
remoteMediaClient = session != null ? session.getRemoteMediaClient() : null; remoteMediaClient = session != null ? session.getRemoteMediaClient() : null;
playbackState = STATE_IDLE;
repeatMode = REPEAT_MODE_OFF;
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY;
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET; pendingSeekPositionMs = C.TIME_UNSET;
updateInternalState(); updateInternalState();
} }
// Media Queue manipulation methods.
/** /**
* Loads media into the receiver app. * Loads a single item media queue. If no session is available, does nothing.
* *
* @param title The title of the media sample. * @param item The item to load.
* @param url The url from which the media is obtained. * @param positionMs The position at which the playback should start in milliseconds relative to
* @param contentMimeType The mime type of the content to play. * the start of the item at {@code startIndex}.
* @param positionMs The position at which the playback should start in milliseconds. * @return The Cast {@code PendingResult}, or null if no session is available.
* @param playWhenReady Whether the player should start playback as soon as it is ready to do so.
*/ */
public void load(String title, String url, String contentMimeType, long positionMs, public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {
boolean playWhenReady) { return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);
lastReportedPositionMs = 0;
if (remoteMediaClient != null) {
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
movieMetadata.putString(MediaMetadata.KEY_TITLE, title);
MediaInfo mediaInfo = new MediaInfo.Builder(url).setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentType(contentMimeType).setMetadata(movieMetadata).build();
remoteMediaClient.load(mediaInfo, playWhenReady, positionMs);
}
} }
/** /**
* Returns whether a cast session is available for playback. * Loads a media queue. If no session is available, does nothing.
*
* @param items The items to load.
* @param startIndex The index of the item at which playback should start.
* @param positionMs The position at which the playback should start in milliseconds relative to
* the start of the item at {@code startIndex}.
* @param repeatMode The repeat mode for the created media queue.
* @return The Cast {@code PendingResult}, or null if no session is available.
*/
public PendingResult<MediaChannelResult> loadItems(MediaQueueItem[] items, int startIndex,
long positionMs, @RepeatMode int repeatMode) {
if (remoteMediaClient != null) {
return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode),
positionMs, null);
}
return null;
}
/**
* Appends a sequence of items to the media queue. If no media queue exists, does nothing.
*
* @param items The items to append.
* @return The Cast {@code PendingResult}, or null if no media queue exists.
*/
public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {
return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
}
/**
* Inserts a sequence of items into the media queue. If no media queue or period with id
* {@code periodId} exist, does nothing.
*
* @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item
* that will follow immediately after the inserted items.
* @param items The items to insert.
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id
* {@code periodId} exist.
*/
public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {
return remoteMediaClient.queueInsertItems(items, periodId, null);
}
return null;
}
/**
* Removes an item from the media queue. If no media queue or period with id {@code periodId}
* exist, does nothing.
*
* @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item
* to remove.
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id
* {@code periodId} exist.
*/
public PendingResult<MediaChannelResult> removeItem(int periodId) {
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
return remoteMediaClient.queueRemoveItem(periodId, null);
}
return null;
}
/**
* Moves an existing item within the media queue. If no media queue or period with id
* {@code periodId} exist, does nothing.
*
* @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item
* to move.
* @param newIndex The target index of the item in the media queue. Must be in the range
* 0 &lt;= index &lt; {@link Timeline#getPeriodCount()}, as provided by
* {@link #getCurrentTimeline()}.
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id
* {@code periodId} exist.
*/
public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
return remoteMediaClient.queueMoveItemToNewIndex(periodId, newIndex, null);
}
return null;
}
/**
* Returns the item that corresponds to the period with the given id, or null if no media queue or
* period with id {@code periodId} exist.
*
* @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item
* to get.
* @return The item that corresponds to the period with the given id, or null if no media queue or
* period with id {@code periodId} exist.
*/
public MediaQueueItem getItem(int periodId) {
MediaStatus mediaStatus = getMediaStatus();
return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
? mediaStatus.getItemById(periodId) : null;
}
// CastSession methods.
/**
* Returns whether a cast session is available.
*/ */
public boolean isCastSessionAvailable() { public boolean isCastSessionAvailable() {
return remoteMediaClient != null; return remoteMediaClient != null;
@ -182,21 +288,7 @@ public final class CastPlayer implements Player {
@Override @Override
public int getPlaybackState() { public int getPlaybackState() {
if (remoteMediaClient == null) { return playbackState;
return STATE_IDLE;
}
int receiverAppStatus = remoteMediaClient.getPlayerState();
switch (receiverAppStatus) {
case MediaStatus.PLAYER_STATE_BUFFERING:
return STATE_BUFFERING;
case MediaStatus.PLAYER_STATE_PLAYING:
case MediaStatus.PLAYER_STATE_PAUSED:
return STATE_READY;
case MediaStatus.PLAYER_STATE_IDLE:
case MediaStatus.PLAYER_STATE_UNKNOWN:
default:
return STATE_IDLE;
}
} }
@Override @Override
@ -213,7 +305,7 @@ public final class CastPlayer implements Player {
@Override @Override
public boolean getPlayWhenReady() { public boolean getPlayWhenReady() {
return remoteMediaClient != null && !remoteMediaClient.isPaused(); return playWhenReady;
} }
@Override @Override
@ -228,13 +320,20 @@ public final class CastPlayer implements Player {
@Override @Override
public void seekTo(long positionMs) { public void seekTo(long positionMs) {
seekTo(0, positionMs); seekTo(getCurrentWindowIndex(), positionMs);
} }
@Override @Override
public void seekTo(int windowIndex, long positionMs) { public void seekTo(int windowIndex, long positionMs) {
if (remoteMediaClient != null) { MediaStatus mediaStatus = getMediaStatus();
if (mediaStatus != null) {
if (getCurrentWindowIndex() != windowIndex) {
remoteMediaClient.queueJumpToItem((int) currentTimeline.getPeriod(windowIndex, period).uid,
positionMs, null).setResultCallback(seekResultCallback);
} else {
remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback); remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback);
}
pendingSeekWindowIndex = windowIndex;
pendingSeekPositionMs = positionMs; pendingSeekPositionMs = positionMs;
for (EventListener listener : listeners) { for (EventListener listener : listeners) {
listener.onPositionDiscontinuity(); listener.onPositionDiscontinuity();
@ -287,47 +386,13 @@ public final class CastPlayer implements Player {
@Override @Override
public void setRepeatMode(@RepeatMode int repeatMode) { public void setRepeatMode(@RepeatMode int repeatMode) {
if (remoteMediaClient != null) { if (remoteMediaClient != null) {
int castRepeatMode; remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), null);
switch (repeatMode) {
case REPEAT_MODE_ONE:
castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_SINGLE;
break;
case REPEAT_MODE_ALL:
castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_ALL;
break;
case REPEAT_MODE_OFF:
castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_OFF;
break;
default:
throw new IllegalArgumentException();
}
remoteMediaClient.queueSetRepeatMode(castRepeatMode, null)
.setResultCallback(repeatModeResultCallback);
} }
} }
@Override @Override
@RepeatMode public int getRepeatMode() { @RepeatMode public int getRepeatMode() {
if (remoteMediaClient == null) { return repeatMode;
return REPEAT_MODE_OFF;
}
MediaStatus mediaStatus = getMediaStatus();
if (mediaStatus == null) {
// No media session active, yet.
return REPEAT_MODE_OFF;
}
int castRepeatMode = mediaStatus.getQueueRepeatMode();
switch (castRepeatMode) {
case MediaStatus.REPEAT_MODE_REPEAT_SINGLE:
return REPEAT_MODE_ONE;
case MediaStatus.REPEAT_MODE_REPEAT_ALL:
case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE:
return REPEAT_MODE_ALL;
case MediaStatus.REPEAT_MODE_REPEAT_OFF:
return REPEAT_MODE_OFF;
default:
throw new IllegalStateException();
}
} }
@Override @Override
@ -363,12 +428,12 @@ public final class CastPlayer implements Player {
@Override @Override
public int getCurrentPeriodIndex() { public int getCurrentPeriodIndex() {
return 0; return getCurrentWindowIndex();
} }
@Override @Override
public int getCurrentWindowIndex() { public int getCurrentWindowIndex() {
return 0; return pendingSeekWindowIndex != C.INDEX_UNSET ? pendingSeekWindowIndex : currentWindowIndex;
} }
@Override @Override
@ -384,14 +449,14 @@ public final class CastPlayer implements Player {
@Override @Override
public long getDuration() { public long getDuration() {
return currentTimeline.isEmpty() ? C.TIME_UNSET return currentTimeline.isEmpty() ? C.TIME_UNSET
: currentTimeline.getWindow(0, window).getDurationMs(); : currentTimeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
} }
@Override @Override
public long getCurrentPosition() { public long getCurrentPosition() {
return remoteMediaClient == null ? lastReportedPositionMs return pendingSeekPositionMs != C.TIME_UNSET ? pendingSeekPositionMs
: pendingSeekPositionMs != C.TIME_UNSET ? pendingSeekPositionMs : remoteMediaClient != null ? remoteMediaClient.getApproximateStreamPosition()
: remoteMediaClient.getApproximateStreamPosition(); : lastReportedPositionMs;
} }
@Override @Override
@ -447,6 +512,121 @@ public final class CastPlayer implements Player {
// Internal methods. // Internal methods.
public void updateInternalState() {
if (remoteMediaClient == null) {
// There is no session. We leave the state of the player as it is now.
return;
}
int playbackState = fetchPlaybackState(remoteMediaClient);
boolean playWhenReady = !remoteMediaClient.isPaused();
if (this.playbackState != playbackState
|| this.playWhenReady != playWhenReady) {
this.playbackState = playbackState;
this.playWhenReady = playWhenReady;
for (EventListener listener : listeners) {
listener.onPlayerStateChanged(this.playWhenReady, this.playbackState);
}
}
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
if (this.repeatMode != repeatMode) {
this.repeatMode = repeatMode;
for (EventListener listener : listeners) {
listener.onRepeatModeChanged(repeatMode);
}
}
int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus());
if (this.currentWindowIndex != currentWindowIndex) {
this.currentWindowIndex = currentWindowIndex;
for (EventListener listener : listeners) {
listener.onPositionDiscontinuity();
}
}
if (updateTracksAndSelections()) {
for (EventListener listener : listeners) {
listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
}
}
maybeUpdateTimelineAndNotify();
}
private void maybeUpdateTimelineAndNotify() {
if (updateTimeline()) {
for (EventListener listener : listeners) {
listener.onTimelineChanged(currentTimeline, null);
}
}
}
/**
* 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;
}
/**
* Updates the internal tracks and selection and returns whether they have changed.
*/
private boolean updateTracksAndSelections() {
if (remoteMediaClient == null) {
// There is no session. We leave the state of the player as it is now.
return false;
}
MediaStatus mediaStatus = getMediaStatus();
MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null;
List<MediaTrack> castMediaTracks = mediaInfo != null ? mediaInfo.getMediaTracks() : null;
if (castMediaTracks == null || castMediaTracks.isEmpty()) {
boolean hasChanged = currentTrackGroups != EMPTY_TRACK_GROUP_ARRAY;
currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY;
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
return hasChanged;
}
long[] activeTrackIds = mediaStatus.getActiveTrackIds();
if (activeTrackIds == null) {
activeTrackIds = EMPTY_TRACK_ID_ARRAY;
}
TrackGroup[] trackGroups = new TrackGroup[castMediaTracks.size()];
TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT];
for (int i = 0; i < castMediaTracks.size(); i++) {
MediaTrack mediaTrack = castMediaTracks.get(i);
trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack));
long id = mediaTrack.getId();
int trackType = MimeTypes.getTrackType(mediaTrack.getContentType());
int rendererIndex = getRendererIndexForTrackType(trackType);
if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET
&& trackSelections[rendererIndex] == null) {
trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0);
}
}
TrackGroupArray newTrackGroups = new TrackGroupArray(trackGroups);
TrackSelectionArray newTrackSelections = new TrackSelectionArray(trackSelections);
if (!newTrackGroups.equals(currentTrackGroups)
|| !newTrackSelections.equals(currentTrackSelection)) {
currentTrackSelection = new TrackSelectionArray(trackSelections);
currentTrackGroups = new TrackGroupArray(trackGroups);
return true;
}
return false;
}
private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) { private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) {
if (this.remoteMediaClient == remoteMediaClient) { if (this.remoteMediaClient == remoteMediaClient) {
// Do nothing. // Do nothing.
@ -463,6 +643,7 @@ public final class CastPlayer implements Player {
} }
remoteMediaClient.addListener(statusListener); remoteMediaClient.addListener(statusListener);
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS); remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
updateInternalState();
} else { } else {
if (sessionAvailabilityListener != null) { if (sessionAvailabilityListener != null) {
sessionAvailabilityListener.onCastSessionUnavailable(); sessionAvailabilityListener.onCastSessionUnavailable();
@ -474,50 +655,58 @@ public final class CastPlayer implements Player {
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null; return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
} }
private @Nullable MediaInfo getMediaInfo() { /**
return remoteMediaClient != null ? remoteMediaClient.getMediaInfo() : null; * Retrieves the playback state from {@code remoteMediaClient} and maps it into a {@link Player}
* state
*/
private static int fetchPlaybackState(RemoteMediaClient remoteMediaClient) {
int receiverAppStatus = remoteMediaClient.getPlayerState();
switch (receiverAppStatus) {
case MediaStatus.PLAYER_STATE_BUFFERING:
return STATE_BUFFERING;
case MediaStatus.PLAYER_STATE_PLAYING:
case MediaStatus.PLAYER_STATE_PAUSED:
return STATE_READY;
case MediaStatus.PLAYER_STATE_IDLE:
case MediaStatus.PLAYER_STATE_UNKNOWN:
default:
return STATE_IDLE;
}
} }
private void updateInternalState() { /**
currentTimeline = Timeline.EMPTY; * Retrieves the repeat mode from {@code remoteMediaClient} and maps it into a
currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY; * {@link Player.RepeatMode}.
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; */
MediaInfo mediaInfo = getMediaInfo(); @RepeatMode
if (mediaInfo == null) { private static int fetchRepeatMode(RemoteMediaClient remoteMediaClient) {
return; MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
if (mediaStatus == null) {
// No media session active, yet.
return REPEAT_MODE_OFF;
}
int castRepeatMode = mediaStatus.getQueueRepeatMode();
switch (castRepeatMode) {
case MediaStatus.REPEAT_MODE_REPEAT_SINGLE:
return REPEAT_MODE_ONE;
case MediaStatus.REPEAT_MODE_REPEAT_ALL:
case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE:
return REPEAT_MODE_ALL;
case MediaStatus.REPEAT_MODE_REPEAT_OFF:
return REPEAT_MODE_OFF;
default:
throw new IllegalStateException();
} }
long streamDurationMs = mediaInfo.getStreamDuration();
boolean isSeekable = streamDurationMs != MediaInfo.UNKNOWN_DURATION;
currentTimeline = new SinglePeriodTimeline(
isSeekable ? C.msToUs(streamDurationMs) : C.TIME_UNSET, isSeekable);
List<MediaTrack> tracks = mediaInfo.getMediaTracks();
if (tracks == null) {
return;
} }
MediaStatus mediaStatus = getMediaStatus(); /**
long[] activeTrackIds = mediaStatus != null ? mediaStatus.getActiveTrackIds() : null; * Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If
if (activeTrackIds == null) { * there is no media session, returns 0.
activeTrackIds = EMPTY_TRACK_ID_ARRAY; */
} private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) {
Integer currentItemId = mediaStatus != null
TrackGroup[] trackGroups = new TrackGroup[tracks.size()]; ? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null;
TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT]; return currentItemId != null ? currentItemId : 0;
for (int i = 0; i < tracks.size(); i++) {
MediaTrack mediaTrack = tracks.get(i);
trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack));
long id = mediaTrack.getId();
int trackType = MimeTypes.getTrackType(mediaTrack.getContentType());
int rendererIndex = getRendererIndexForTrackType(trackType);
if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET
&& trackSelections[rendererIndex] == null) {
trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0);
}
}
currentTrackSelection = new TrackSelectionArray(trackSelections);
currentTrackGroups = new TrackGroupArray(trackGroups);
} }
private static boolean isTrackActive(long id, long[] activeTrackIds) { private static boolean isTrackActive(long id, long[] activeTrackIds) {
@ -536,6 +725,19 @@ public final class CastPlayer implements Player {
: C.INDEX_UNSET; : C.INDEX_UNSET;
} }
private static int getCastRepeatMode(@RepeatMode int repeatMode) {
switch (repeatMode) {
case REPEAT_MODE_ONE:
return MediaStatus.REPEAT_MODE_REPEAT_SINGLE;
case REPEAT_MODE_ALL:
return MediaStatus.REPEAT_MODE_REPEAT_ALL;
case REPEAT_MODE_OFF:
return MediaStatus.REPEAT_MODE_REPEAT_OFF;
default:
throw new IllegalArgumentException();
}
}
private final class StatusListener implements RemoteMediaClient.Listener, private final class StatusListener implements RemoteMediaClient.Listener,
SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener { SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener {
@ -550,24 +752,16 @@ public final class CastPlayer implements Player {
@Override @Override
public void onStatusUpdated() { public void onStatusUpdated() {
boolean playWhenReady = getPlayWhenReady();
int playbackState = getPlaybackState();
for (EventListener listener : listeners) {
listener.onPlayerStateChanged(playWhenReady, playbackState);
}
}
@Override
public void onMetadataUpdated() {
updateInternalState(); updateInternalState();
for (EventListener listener : listeners) {
listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
listener.onTimelineChanged(currentTimeline, null);
}
} }
@Override @Override
public void onQueueStatusUpdated() {} public void onMetadataUpdated() {}
@Override
public void onQueueStatusUpdated() {
maybeUpdateTimelineAndNotify();
}
@Override @Override
public void onPreloadStatusUpdated() {} public void onPreloadStatusUpdated() {}
@ -632,38 +826,22 @@ public final class CastPlayer implements Player {
// Result callbacks hooks. // Result callbacks hooks.
private final class RepeatModeResultCallback implements ResultCallback<MediaChannelResult> {
@Override
public void onResult(MediaChannelResult result) {
int statusCode = result.getStatus().getStatusCode();
if (statusCode == CommonStatusCodes.SUCCESS) {
int repeatMode = getRepeatMode();
for (EventListener listener : listeners) {
listener.onRepeatModeChanged(repeatMode);
}
} else {
Log.e(TAG, "Set repeat mode failed. Error code " + statusCode + ": "
+ CastUtils.getLogString(statusCode));
}
}
}
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> { private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
@Override @Override
public void onResult(MediaChannelResult result) { public void onResult(@NonNull MediaChannelResult result) {
int statusCode = result.getStatus().getStatusCode(); int statusCode = result.getStatus().getStatusCode();
if (statusCode == CommonStatusCodes.SUCCESS) { if (statusCode == CastStatusCodes.REPLACED) {
pendingSeekPositionMs = C.TIME_UNSET;
} else if (statusCode == CastStatusCodes.REPLACED) {
// A seek was executed before this one completed. Do nothing. // A seek was executed before this one completed. Do nothing.
} else { } else {
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
if (statusCode != CommonStatusCodes.SUCCESS) {
Log.e(TAG, "Seek failed. Error code " + statusCode + ": " Log.e(TAG, "Seek failed. Error code " + statusCode + ": "
+ CastUtils.getLogString(statusCode)); + CastUtils.getLogString(statusCode));
} }
} }
}
} }

View File

@ -0,0 +1,114 @@
/*
* Copyright (C) 2017 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 android.util.SparseIntArray;
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.Collections;
import java.util.List;
/**
* A {@link Timeline} for Cast media queues.
*/
/* package */ final class CastTimeline extends Timeline {
public static final CastTimeline EMPTY_CAST_TIMELINE =
new CastTimeline(Collections.<MediaQueueItem>emptyList());
private final SparseIntArray idsToIndex;
private final int[] ids;
private final long[] durationsUs;
private final long[] defaultPositionsUs;
public CastTimeline(List<MediaQueueItem> items) {
int itemCount = items.size();
int index = 0;
idsToIndex = new SparseIntArray(itemCount);
ids = new int[itemCount];
durationsUs = new long[itemCount];
defaultPositionsUs = new long[itemCount];
for (MediaQueueItem item : items) {
int itemId = item.getItemId();
ids[index] = itemId;
idsToIndex.put(itemId, index);
durationsUs[index] = getStreamDurationUs(item.getMedia());
defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
index++;
}
}
@Override
public int getWindowCount() {
return ids.length;
}
@Override
public Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs) {
long durationUs = durationsUs[windowIndex];
boolean isDynamic = durationUs == C.TIME_UNSET;
return window.set(ids[windowIndex], C.TIME_UNSET, C.TIME_UNSET, !isDynamic, isDynamic,
defaultPositionsUs[windowIndex], durationUs, windowIndex, windowIndex, 0);
}
@Override
public int getPeriodCount() {
return ids.length;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
int id = ids[periodIndex];
return period.set(id, id, periodIndex, durationsUs[periodIndex], 0);
}
@Override
public int getIndexOfPeriod(Object uid) {
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++;
}
return true;
}
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;
}
}

View File

@ -198,7 +198,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
/** /**
* Returns the {@link MediaSource} at a specified index. * Returns the {@link MediaSource} at a specified index.
* *
* @param index A index in the range of 0 &lt;= index &lt;= {@link #getSize()}. * @param index An index in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @return The {@link MediaSource} at this index. * @return The {@link MediaSource} at this index.
*/ */
public synchronized MediaSource getMediaSource(int index) { public synchronized MediaSource getMediaSource(int index) {