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:
- *
- * - Part of the Cast API is not exposed through this interface. For instance, volume settings
- * and track selection.
- * - Repeat mode is not working. See [internal: b/64137174].
- *
+ * 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) {