diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java index f819e54e50..68c5904362 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java @@ -52,17 +52,17 @@ import java.util.List; /** * 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 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.name = name; - this.type = type; + this.mimeType = mimeType; } @Override diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 741df7eff1..8b461ec65c 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -37,6 +37,9 @@ import com.google.android.exoplayer2.ui.PlaybackControlView; import com.google.android.exoplayer2.ui.SimpleExoPlayerView; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; 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; /** @@ -95,12 +98,12 @@ import com.google.android.gms.cast.framework.CastContext; boolean playWhenReady) { this.currentSample = currentSample; if (playbackLocation == PLAYBACK_REMOTE) { - castPlayer.load(currentSample.name, currentSample.uri, currentSample.type, positionMs, - playWhenReady); + castPlayer.loadItem(buildMediaQueueItem(currentSample), positionMs); + castPlayer.setPlayWhenReady(playWhenReady); } else /* playbackLocation == PLAYBACK_LOCAL */ { + exoPlayer.prepare(buildMediaSource(currentSample), true, true); exoPlayer.setPlayWhenReady(playWhenReady); exoPlayer.seekTo(positionMs); - exoPlayer.prepare(buildMediaSource(currentSample), true, true); } } @@ -143,9 +146,18 @@ import com.google.android.gms.cast.framework.CastContext; // 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) { Uri uri = Uri.parse(sample.uri); - switch (sample.type) { + switch (sample.mimeType) { case CastDemoUtil.MIME_TYPE_SS: return new SsMediaSource(uri, DATA_SOURCE_FACTORY, 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(), null, null); 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(); } - long playbackPositionMs = 0; - boolean playWhenReady = true; - if (exoPlayer != null) { + long playbackPositionMs; + boolean playWhenReady; + if (this.playbackLocation == PLAYBACK_LOCAL) { playbackPositionMs = exoPlayer.getCurrentPosition(); playWhenReady = exoPlayer.getPlayWhenReady(); - } else if (this.playbackLocation == PLAYBACK_REMOTE) { + exoPlayer.stop(); + } else /* this.playbackLocation == PLAYBACK_REMOTE */ { playbackPositionMs = castPlayer.getCurrentPosition(); playWhenReady = castPlayer.getPlayWhenReady(); + castPlayer.stop(); } this.playbackLocation = playbackLocation; diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml index 7e39320e3b..5d94931b64 100644 --- a/demos/cast/src/main/res/layout/main_activity.xml +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -35,7 +35,8 @@ android:id="@+id/cast_control_view" android:layout_width="match_parent" android:layout_height="0dp" - app:show_timeout="-1" android:layout_weight="2" - android:visibility="gone"/> + android:visibility="gone" + app:repeat_toggle_modes="all|one" + app:show_timeout="-1"/> diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index e79fef74d5..234b8384f9 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -15,23 +15,24 @@ */ package com.google.android.exoplayer2.ext.cast; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; 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.TrackGroupArray; import com.google.android.exoplayer2.trackselection.FixedTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; 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.Util; import com.google.android.gms.cast.CastStatusCodes; 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.MediaTrack; 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.MediaChannelResult; 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 java.util.List; import java.util.concurrent.CopyOnWriteArraySet; @@ -48,19 +50,16 @@ import java.util.concurrent.CopyOnWriteArraySet; /** * {@link Player} implementation that communicates with a Cast receiver app. * - *

Calls to the methods in this class depend on the availability of an underlying cast session. - * If no session is available, method calls have no effect. To keep track of the underyling session, + *

The behavior of this class depends on the underlying Cast session, which is obtained from the + * Cast context passed to {@link #CastPlayer}. To keep track of the session, * {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be - * implemented and attached to the player. + * implemented and attached to the player.

* - *

Methods should be called on the application's main thread. + *

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.

* - *

Known issues: - *

+ *

Methods should be called on the application's main thread.

*/ public final class CastPlayer implements Player { @@ -95,10 +94,12 @@ public final class CastPlayer implements Player { private final CastContext castContext; private final Timeline.Window window; + private final Timeline.Period period; + + private RemoteMediaClient remoteMediaClient; // Result callbacks. private final StatusListener statusListener; - private final RepeatModeResultCallback repeatModeResultCallback; private final SeekResultCallback seekResultCallback; // Listeners. @@ -106,11 +107,15 @@ public final class CastPlayer implements Player { private SessionAvailabilityListener sessionAvailabilityListener; // Internal state. - private RemoteMediaClient remoteMediaClient; - private Timeline currentTimeline; + private CastTimeline currentTimeline; private TrackGroupArray currentTrackGroups; private TrackSelectionArray currentTrackSelection; + private int playbackState; + private int repeatMode; + private int currentWindowIndex; + private boolean playWhenReady; private long lastReportedPositionMs; + private int pendingSeekWindowIndex; private long pendingSeekPositionMs; /** @@ -119,41 +124,142 @@ public final class CastPlayer implements Player { public CastPlayer(CastContext castContext) { this.castContext = castContext; window = new Timeline.Window(); + period = new Timeline.Period(); statusListener = new StatusListener(); - repeatModeResultCallback = new RepeatModeResultCallback(); seekResultCallback = new SeekResultCallback(); listeners = new CopyOnWriteArraySet<>(); + SessionManager sessionManager = castContext.getSessionManager(); sessionManager.addSessionManagerListener(statusListener, CastSession.class); CastSession session = sessionManager.getCurrentCastSession(); 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; 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 url The url from which the media is obtained. - * @param contentMimeType The mime type of the content to play. - * @param positionMs The position at which the playback should start in milliseconds. - * @param playWhenReady Whether the player should start playback as soon as it is ready to do so. + * @param item The item to load. + * @param positionMs The position at which the playback should start in milliseconds relative to + * the start of the item at {@code startIndex}. + * @return The Cast {@code PendingResult}, or null if no session is available. */ - public void load(String title, String url, String contentMimeType, long positionMs, - boolean playWhenReady) { - 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); - } + public PendingResult loadItem(MediaQueueItem item, long positionMs) { + return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF); } /** - * 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 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 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 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 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 <= index < {@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 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() { return remoteMediaClient != null; @@ -182,21 +288,7 @@ public final class CastPlayer implements Player { @Override public int getPlaybackState() { - if (remoteMediaClient == null) { - 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; - } + return playbackState; } @Override @@ -213,7 +305,7 @@ public final class CastPlayer implements Player { @Override public boolean getPlayWhenReady() { - return remoteMediaClient != null && !remoteMediaClient.isPaused(); + return playWhenReady; } @Override @@ -228,13 +320,20 @@ public final class CastPlayer implements Player { @Override public void seekTo(long positionMs) { - seekTo(0, positionMs); + seekTo(getCurrentWindowIndex(), positionMs); } @Override public void seekTo(int windowIndex, long positionMs) { - if (remoteMediaClient != null) { - remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback); + 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); + } + pendingSeekWindowIndex = windowIndex; pendingSeekPositionMs = positionMs; for (EventListener listener : listeners) { listener.onPositionDiscontinuity(); @@ -287,47 +386,13 @@ public final class CastPlayer implements Player { @Override public void setRepeatMode(@RepeatMode int repeatMode) { if (remoteMediaClient != null) { - int castRepeatMode; - 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); + remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), null); } } @Override @RepeatMode public int getRepeatMode() { - if (remoteMediaClient == null) { - 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(); - } + return repeatMode; } @Override @@ -363,12 +428,12 @@ public final class CastPlayer implements Player { @Override public int getCurrentPeriodIndex() { - return 0; + return getCurrentWindowIndex(); } @Override public int getCurrentWindowIndex() { - return 0; + return pendingSeekWindowIndex != C.INDEX_UNSET ? pendingSeekWindowIndex : currentWindowIndex; } @Override @@ -384,14 +449,14 @@ public final class CastPlayer implements Player { @Override public long getDuration() { return currentTimeline.isEmpty() ? C.TIME_UNSET - : currentTimeline.getWindow(0, window).getDurationMs(); + : currentTimeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); } @Override public long getCurrentPosition() { - return remoteMediaClient == null ? lastReportedPositionMs - : pendingSeekPositionMs != C.TIME_UNSET ? pendingSeekPositionMs - : remoteMediaClient.getApproximateStreamPosition(); + return pendingSeekPositionMs != C.TIME_UNSET ? pendingSeekPositionMs + : remoteMediaClient != null ? remoteMediaClient.getApproximateStreamPosition() + : lastReportedPositionMs; } @Override @@ -447,6 +512,121 @@ public final class CastPlayer implements Player { // 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 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 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) { if (this.remoteMediaClient == remoteMediaClient) { // Do nothing. @@ -463,6 +643,7 @@ public final class CastPlayer implements Player { } remoteMediaClient.addListener(statusListener); remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS); + updateInternalState(); } else { if (sessionAvailabilityListener != null) { sessionAvailabilityListener.onCastSessionUnavailable(); @@ -474,50 +655,58 @@ public final class CastPlayer implements Player { 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; - currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY; - currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; - MediaInfo mediaInfo = getMediaInfo(); - if (mediaInfo == null) { - return; + /** + * Retrieves the repeat mode from {@code remoteMediaClient} and maps it into a + * {@link Player.RepeatMode}. + */ + @RepeatMode + private static int fetchRepeatMode(RemoteMediaClient remoteMediaClient) { + MediaStatus mediaStatus = remoteMediaClient.getMediaStatus(); + if (mediaStatus == null) { + // No media session active, yet. + return REPEAT_MODE_OFF; } - long streamDurationMs = mediaInfo.getStreamDuration(); - boolean isSeekable = streamDurationMs != MediaInfo.UNKNOWN_DURATION; - currentTimeline = new SinglePeriodTimeline( - isSeekable ? C.msToUs(streamDurationMs) : C.TIME_UNSET, isSeekable); - - List tracks = mediaInfo.getMediaTracks(); - if (tracks == null) { - return; + 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(); } + } - MediaStatus mediaStatus = getMediaStatus(); - long[] activeTrackIds = mediaStatus != null ? mediaStatus.getActiveTrackIds() : null; - if (activeTrackIds == null) { - activeTrackIds = EMPTY_TRACK_ID_ARRAY; - } - - TrackGroup[] trackGroups = new TrackGroup[tracks.size()]; - TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT]; - 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); + /** + * Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If + * there is no media session, returns 0. + */ + private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) { + Integer currentItemId = mediaStatus != null + ? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null; + return currentItemId != null ? currentItemId : 0; } private static boolean isTrackActive(long id, long[] activeTrackIds) { @@ -536,6 +725,19 @@ public final class CastPlayer implements Player { : 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, SessionManagerListener, RemoteMediaClient.ProgressListener { @@ -550,24 +752,16 @@ public final class CastPlayer implements Player { @Override public void onStatusUpdated() { - boolean playWhenReady = getPlayWhenReady(); - int playbackState = getPlaybackState(); - for (EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, playbackState); - } - } - - @Override - public void onMetadataUpdated() { updateInternalState(); - for (EventListener listener : listeners) { - listener.onTracksChanged(currentTrackGroups, currentTrackSelection); - listener.onTimelineChanged(currentTimeline, null); - } } @Override - public void onQueueStatusUpdated() {} + public void onMetadataUpdated() {} + + @Override + public void onQueueStatusUpdated() { + maybeUpdateTimelineAndNotify(); + } @Override public void onPreloadStatusUpdated() {} @@ -632,36 +826,20 @@ public final class CastPlayer implements Player { // Result callbacks hooks. - private final class RepeatModeResultCallback implements ResultCallback { - - @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 { @Override - public void onResult(MediaChannelResult result) { + public void onResult(@NonNull MediaChannelResult result) { int statusCode = result.getStatus().getStatusCode(); - if (statusCode == CommonStatusCodes.SUCCESS) { - pendingSeekPositionMs = C.TIME_UNSET; - } else if (statusCode == CastStatusCodes.REPLACED) { + if (statusCode == CastStatusCodes.REPLACED) { // A seek was executed before this one completed. Do nothing. } else { - Log.e(TAG, "Seek failed. Error code " + statusCode + ": " - + CastUtils.getLogString(statusCode)); + pendingSeekWindowIndex = C.INDEX_UNSET; + pendingSeekPositionMs = C.TIME_UNSET; + if (statusCode != CommonStatusCodes.SUCCESS) { + Log.e(TAG, "Seek failed. Error code " + statusCode + ": " + + CastUtils.getLogString(statusCode)); + } } } diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java new file mode 100644 index 0000000000..39b57148b2 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java @@ -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.emptyList()); + + private final SparseIntArray idsToIndex; + private final int[] ids; + private final long[] durationsUs; + private final long[] defaultPositionsUs; + + public CastTimeline(List 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 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; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 8614cf9c85..36c674f81a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -198,7 +198,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl /** * Returns the {@link MediaSource} at a specified index. * - * @param index A index in the range of 0 <= index <= {@link #getSize()}. + * @param index An index in the range of 0 <= index <= {@link #getSize()}. * @return The {@link MediaSource} at this index. */ public synchronized MediaSource getMediaSource(int index) {