diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index a9fa27ad58..8f074c9238 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -48,4 +48,5 @@ dependencies { compile project(modulePrefix + 'library-smoothstreaming') compile project(modulePrefix + 'library-ui') compile project(modulePrefix + 'extension-cast') + compile 'com.android.support:recyclerview-v7:' + supportLibraryVersion } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 094e9f9e6e..d34888352f 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -15,37 +15,54 @@ */ package com.google.android.exoplayer2.castdemo; -import android.graphics.Color; +import android.content.Context; import android.os.Bundle; -import android.support.annotation.NonNull; +import android.support.v4.graphics.ColorUtils; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.support.v7.widget.helper.ItemTouchHelper; import android.view.KeyEvent; +import android.view.LayoutInflater; import android.view.Menu; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.ListView; +import android.widget.TextView; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.ext.cast.CastPlayer; import com.google.android.exoplayer2.ui.PlaybackControlView; import com.google.android.exoplayer2.ui.SimpleExoPlayerView; import com.google.android.gms.cast.framework.CastButtonFactory; +import com.google.android.gms.cast.framework.CastContext; /** * An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}. */ -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity implements OnClickListener, + PlayerManager.QueuePositionListener { private SimpleExoPlayerView simpleExoPlayerView; private PlaybackControlView castControlView; private PlayerManager playerManager; + private MediaQueueAdapter listAdapter; + private CastContext castContext; // Activity lifecycle methods. @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Getting the cast context later than onStart can cause device discovery not to take place. + castContext = CastContext.getSharedInstance(this); setContentView(R.layout.main_activity); @@ -54,24 +71,30 @@ public class MainActivity extends AppCompatActivity { castControlView = findViewById(R.id.cast_control_view); - ListView sampleList = findViewById(R.id.sample_list); - sampleList.setAdapter(new SampleListAdapter()); - sampleList.setOnItemClickListener(new SampleClickListener()); + RecyclerView sampleList = findViewById(R.id.sample_list); + ItemTouchHelper helper = new ItemTouchHelper(new RecyclerViewCallback()); + helper.attachToRecyclerView(sampleList); + sampleList.setLayoutManager(new LinearLayoutManager(this)); + sampleList.setHasFixedSize(true); + listAdapter = new MediaQueueAdapter(); + sampleList.setAdapter(listAdapter); + + findViewById(R.id.add_sample_button).setOnClickListener(this); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.menu, menu); - CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu, - R.id.media_route_menu_item); + CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item); return true; } @Override public void onResume() { super.onResume(); - playerManager = new PlayerManager(simpleExoPlayerView, castControlView, this); + playerManager = PlayerManager.createPlayerManager(this, simpleExoPlayerView, castControlView, + this, castContext); } @Override @@ -89,32 +112,141 @@ public class MainActivity extends AppCompatActivity { return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event); } - // User controls. + @Override + public void onClick(View view) { + new AlertDialog.Builder(this).setTitle(R.string.sample_list_dialog_title) + .setView(buildSampleListView()).setPositiveButton(android.R.string.ok, null).create() + .show(); + } - private final class SampleListAdapter extends ArrayAdapter { + // PlayerManager.QueuePositionListener implementation. - public SampleListAdapter() { - super(getApplicationContext(), android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); + @Override + public void onQueuePositionChanged(int previousIndex, int newIndex) { + if (previousIndex != C.INDEX_UNSET) { + listAdapter.notifyItemChanged(previousIndex); + } + if (newIndex != C.INDEX_UNSET) { + listAdapter.notifyItemChanged(newIndex); + } + } + + // Internal methods. + + private View buildSampleListView() { + View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null); + ListView sampleList = dialogList.findViewById(R.id.sample_list); + sampleList.setAdapter(new SampleListAdapter(this)); + sampleList.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + playerManager.addItem(DemoUtil.SAMPLES.get(position)); + listAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); + } + + }); + return dialogList; + } + + // Internal classes. + + private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener { + + public final TextView textView; + + public QueueItemViewHolder(TextView textView) { + super(textView); + this.textView = textView; + textView.setOnClickListener(this); } @Override - @NonNull - public View getView(int position, View convertView, @NonNull ViewGroup parent) { - View view = super.getView(position, convertView, parent); - view.setBackgroundColor(Color.WHITE); - return view; + public void onClick(View v) { + playerManager.selectQueueItem(getAdapterPosition()); } } - private class SampleClickListener implements AdapterView.OnItemClickListener { + private class MediaQueueAdapter extends RecyclerView.Adapter { @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - if (parent.getSelectedItemPosition() != position) { - DemoUtil.Sample currentSample = DemoUtil.SAMPLES.get(position); - playerManager.setCurrentSample(currentSample, 0, true); + public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + TextView v = (TextView) LayoutInflater.from(parent.getContext()) + .inflate(android.R.layout.simple_list_item_1, parent, false); + return new QueueItemViewHolder(v); + } + + @Override + public void onBindViewHolder(QueueItemViewHolder holder, int position) { + TextView view = holder.textView; + view.setText(playerManager.getItem(position).name); + // TODO: Solve coloring using the theme's ColorStateList. + view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(), + position == playerManager.getCurrentItemIndex() ? 255 : 100)); + } + + @Override + public int getItemCount() { + return playerManager.getMediaQueueSize(); + } + + } + + private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback { + + private int draggingFromPosition; + private int draggingToPosition; + + public RecyclerViewCallback() { + super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END); + draggingFromPosition = C.INDEX_UNSET; + draggingToPosition = C.INDEX_UNSET; + } + + @Override + public boolean onMove(RecyclerView list, RecyclerView.ViewHolder origin, + RecyclerView.ViewHolder target) { + int fromPosition = origin.getAdapterPosition(); + int toPosition = target.getAdapterPosition(); + if (draggingFromPosition == C.INDEX_UNSET) { + // A drag has started, but changes to the media queue will be reflected in clearView(). + draggingFromPosition = fromPosition; } + draggingToPosition = toPosition; + listAdapter.notifyItemMoved(fromPosition, toPosition); + return true; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + int position = viewHolder.getAdapterPosition(); + if (playerManager.removeItem(position)) { + listAdapter.notifyItemRemoved(position); + } + } + + @Override + public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + if (draggingFromPosition != C.INDEX_UNSET) { + // A drag has ended. We reflect the media queue change in the player. + if (!playerManager.moveItem(draggingFromPosition, draggingToPosition)) { + // The move failed. The entire sequence of onMove calls since the drag started needs to be + // invalidated. + listAdapter.notifyDataSetChanged(); + } + } + draggingFromPosition = C.INDEX_UNSET; + draggingToPosition = C.INDEX_UNSET; + } + + } + + private static final class SampleListAdapter extends ArrayAdapter { + + public SampleListAdapter(Context context) { + super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); } } 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 f00d27a067..0f4adfae99 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 @@ -19,11 +19,19 @@ import android.content.Context; import android.net.Uri; import android.view.KeyEvent; import android.view.View; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DefaultEventListener; +import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.ext.cast.CastPlayer; +import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; @@ -40,14 +48,25 @@ 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 java.util.ArrayList; /** - * Manages players for the ExoPlayer/Cast integration app. + * Manages players and an internal media queue for the ExoPlayer/Cast demo app. */ -/* package */ final class PlayerManager implements CastPlayer.SessionAvailabilityListener { +/* package */ final class PlayerManager extends DefaultEventListener + implements CastPlayer.SessionAvailabilityListener { - private static final int PLAYBACK_REMOTE = 1; - private static final int PLAYBACK_LOCAL = 2; + /** + * Listener for changes in the media queue playback position. + */ + public interface QueuePositionListener { + + /** + * Called when the currently played item of the media queue changes. + */ + void onQueuePositionChanged(int previousIndex, int newIndex); + + } private static final String USER_AGENT = "ExoCastDemoPlayer"; private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); @@ -58,64 +77,174 @@ import com.google.android.gms.cast.framework.CastContext; private final PlaybackControlView castControlView; private final SimpleExoPlayer exoPlayer; private final CastPlayer castPlayer; + private final ArrayList mediaQueue; + private final QueuePositionListener listener; - private int playbackLocation; - private DemoUtil.Sample currentSample; + private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource; + private boolean castMediaQueueCreationPending; + private int currentItemIndex; + private Player currentPlayer; /** + * @param listener A {@link QueuePositionListener} for queue position changes. * @param exoPlayerView The {@link SimpleExoPlayerView} for local playback. * @param castControlView The {@link PlaybackControlView} to control remote playback. * @param context A {@link Context}. + * @param castContext The {@link CastContext}. */ - public PlayerManager(SimpleExoPlayerView exoPlayerView, PlaybackControlView castControlView, - Context context) { + public static PlayerManager createPlayerManager(QueuePositionListener listener, + SimpleExoPlayerView exoPlayerView, PlaybackControlView castControlView, Context context, + CastContext castContext) { + PlayerManager playerManager = new PlayerManager(listener, exoPlayerView, castControlView, + context, castContext); + playerManager.init(); + return playerManager; + } + + private PlayerManager(QueuePositionListener listener, SimpleExoPlayerView exoPlayerView, + PlaybackControlView castControlView, Context context, CastContext castContext) { + this.listener = listener; this.exoPlayerView = exoPlayerView; this.castControlView = castControlView; + mediaQueue = new ArrayList<>(); + currentItemIndex = C.INDEX_UNSET; DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER); RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null); exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); + exoPlayer.addListener(this); exoPlayerView.setPlayer(exoPlayer); - castPlayer = new CastPlayer(CastContext.getSharedInstance(context)); + castPlayer = new CastPlayer(castContext); + castPlayer.addListener(this); castPlayer.setSessionAvailabilityListener(this); castControlView.setPlayer(castPlayer); + } - setPlaybackLocation(castPlayer.isCastSessionAvailable() ? PLAYBACK_REMOTE : PLAYBACK_LOCAL); + // Queue manipulation methods. + + /** + * Plays a specified queue item in the current player. + * + * @param itemIndex The index of the item to play. + */ + public void selectQueueItem(int itemIndex) { + setCurrentItem(itemIndex, C.TIME_UNSET, true); } /** - * Starts playback of the given sample at the given position. - * - * @param currentSample The {@link DemoUtil} to play. - * @param positionMs The position at which playback should start. - * @param playWhenReady Whether the player should proceed when ready to do so. + * Returns the index of the currently played item. */ - public void setCurrentSample(DemoUtil.Sample currentSample, long positionMs, - boolean playWhenReady) { - this.currentSample = currentSample; - if (playbackLocation == PLAYBACK_REMOTE) { - castPlayer.loadItem(buildMediaQueueItem(currentSample), positionMs); - castPlayer.setPlayWhenReady(playWhenReady); - } else /* playbackLocation == PLAYBACK_LOCAL */ { - exoPlayer.prepare(buildMediaSource(currentSample), true, true); - exoPlayer.setPlayWhenReady(playWhenReady); - exoPlayer.seekTo(positionMs); + public int getCurrentItemIndex() { + return currentItemIndex; + } + + /** + * Appends {@code sample} to the media queue. + * + * @param sample The {@link Sample} to append. + */ + public void addItem(Sample sample) { + mediaQueue.add(sample); + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(sample)); + } else { + castPlayer.addItems(buildMediaQueueItem(sample)); } } /** - * Dispatches a given {@link KeyEvent} to whichever view corresponds according to the current - * playback location. + * Returns the size of the media queue. + */ + public int getMediaQueueSize() { + return mediaQueue.size(); + } + + /** + * Returns the item at the given index in the media queue. + * + * @param position The index of the item. + * @return The item at the given index in the media queue. + */ + public Sample getItem(int position) { + return mediaQueue.get(position); + } + + /** + * Removes the item at the given index from the media queue. + * + * @param itemIndex The index of the item to remove. + * @return Whether the removal was successful. + */ + public boolean removeItem(int itemIndex) { + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource.removeMediaSource(itemIndex); + } else { + if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + if (castTimeline.getPeriodCount() <= itemIndex) { + return false; + } + castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id); + } + } + mediaQueue.remove(itemIndex); + if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { + maybeSetCurrentItemAndNotify(C.INDEX_UNSET); + } else if (itemIndex < currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } + return true; + } + + /** + * Moves an item within the queue. + * + * @param fromIndex The index of the item to move. + * @param toIndex The target index of the item in the queue. + * @return Whether the item move was successful. + */ + public boolean moveItem(int fromIndex, int toIndex) { + // Player update. + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource.moveMediaSource(fromIndex, toIndex); + } else if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + int periodCount = castTimeline.getPeriodCount(); + if (periodCount <= fromIndex || periodCount <= toIndex) { + return false; + } + int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id; + castPlayer.moveItem(elementId, toIndex); + } + + mediaQueue.add(toIndex, mediaQueue.remove(fromIndex)); + + // Index update. + if (fromIndex == currentItemIndex) { + maybeSetCurrentItemAndNotify(toIndex); + } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex + 1); + } + + return true; + } + + // Miscellaneous methods. + + /** + * Dispatches a given {@link KeyEvent} to the corresponding view of the current player. * * @param event The {@link KeyEvent}. * @return Whether the event was handled by the target view. */ public boolean dispatchKeyEvent(KeyEvent event) { - if (playbackLocation == PLAYBACK_REMOTE) { - return castControlView.dispatchKeyEvent(event); - } else /* playbackLocation == PLAYBACK_REMOTE */ { + if (currentPlayer == exoPlayer) { return exoPlayerView.dispatchKeyEvent(event); + } else /* currentPlayer == castPlayer */ { + return castControlView.dispatchKeyEvent(event); } } @@ -123,33 +252,136 @@ import com.google.android.gms.cast.framework.CastContext; * Releases the manager and the players that it holds. */ public void release() { + currentItemIndex = C.INDEX_UNSET; + mediaQueue.clear(); castPlayer.setSessionAvailabilityListener(null); castPlayer.release(); exoPlayerView.setPlayer(null); exoPlayer.release(); } + // Player.EventListener implementation. + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + updateCurrentItemIndex(); + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + updateCurrentItemIndex(); + } + // CastPlayer.SessionAvailabilityListener implementation. @Override public void onCastSessionAvailable() { - setPlaybackLocation(PLAYBACK_REMOTE); + setCurrentPlayer(castPlayer); } @Override public void onCastSessionUnavailable() { - setPlaybackLocation(PLAYBACK_LOCAL); + setCurrentPlayer(exoPlayer); } // Internal methods. - private static MediaQueueItem buildMediaQueueItem(DemoUtil.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 void init() { + setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); + } + + private void updateCurrentItemIndex() { + int playbackState = currentPlayer.getPlaybackState(); + maybeSetCurrentItemAndNotify( + playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED + ? currentPlayer.getCurrentWindowIndex() : C.INDEX_UNSET); + } + + private void setCurrentPlayer(Player currentPlayer) { + if (this.currentPlayer == currentPlayer) { + return; + } + + // View management. + if (currentPlayer == exoPlayer) { + exoPlayerView.setVisibility(View.VISIBLE); + castControlView.hide(); + } else /* currentPlayer == castPlayer */ { + exoPlayerView.setVisibility(View.GONE); + castControlView.show(); + } + + // Player state management. + long playbackPositionMs = C.TIME_UNSET; + int windowIndex = C.INDEX_UNSET; + boolean playWhenReady = false; + if (this.currentPlayer != null) { + int playbackState = this.currentPlayer.getPlaybackState(); + if (playbackState != Player.STATE_ENDED) { + playbackPositionMs = this.currentPlayer.getCurrentPosition(); + playWhenReady = this.currentPlayer.getPlayWhenReady(); + windowIndex = this.currentPlayer.getCurrentWindowIndex(); + if (windowIndex != currentItemIndex) { + playbackPositionMs = C.TIME_UNSET; + windowIndex = currentItemIndex; + } + } + this.currentPlayer.stop(true); + } else { + // This is the initial setup. No need to save any state. + } + + this.currentPlayer = currentPlayer; + + // Media queue management. + castMediaQueueCreationPending = currentPlayer == castPlayer; + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource = new DynamicConcatenatingMediaSource(); + for (int i = 0; i < mediaQueue.size(); i++) { + dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(mediaQueue.get(i))); + } + exoPlayer.prepare(dynamicConcatenatingMediaSource); + } + + // Playback transition. + if (windowIndex != C.INDEX_UNSET) { + setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); + } + } + + /** + * Starts playback of the item at the given position. + * + * @param itemIndex The index of the item to play. + * @param positionMs The position at which playback should start. + * @param playWhenReady Whether the player should proceed when ready to do so. + */ + private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { + maybeSetCurrentItemAndNotify(itemIndex); + if (castMediaQueueCreationPending) { + MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; + for (int i = 0; i < items.length; i++) { + items[i] = buildMediaQueueItem(mediaQueue.get(i)); + } + castMediaQueueCreationPending = false; + castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); + } else { + currentPlayer.seekTo(itemIndex, positionMs); + currentPlayer.setPlayWhenReady(playWhenReady); + } + } + + private void maybeSetCurrentItemAndNotify(int currentItemIndex) { + if (this.currentItemIndex != currentItemIndex) { + int oldIndex = this.currentItemIndex; + this.currentItemIndex = currentItemIndex; + listener.onQueuePositionChanged(oldIndex, currentItemIndex); + } } private static MediaSource buildMediaSource(DemoUtil.Sample sample) { @@ -177,36 +409,13 @@ import com.google.android.gms.cast.framework.CastContext; } } - private void setPlaybackLocation(int playbackLocation) { - if (this.playbackLocation == playbackLocation) { - return; - } - - // View management. - if (playbackLocation == PLAYBACK_LOCAL) { - exoPlayerView.setVisibility(View.VISIBLE); - castControlView.hide(); - } else { - exoPlayerView.setVisibility(View.GONE); - castControlView.show(); - } - - long playbackPositionMs; - boolean playWhenReady; - if (this.playbackLocation == PLAYBACK_LOCAL) { - playbackPositionMs = exoPlayer.getCurrentPosition(); - playWhenReady = exoPlayer.getPlayWhenReady(); - exoPlayer.stop(); - } else /* this.playbackLocation == PLAYBACK_REMOTE */ { - playbackPositionMs = castPlayer.getCurrentPosition(); - playWhenReady = castPlayer.getPlayWhenReady(); - castPlayer.stop(); - } - - this.playbackLocation = playbackLocation; - if (currentSample != null) { - setCurrentSample(currentSample, playbackPositionMs, playWhenReady); - } + private static MediaQueueItem buildMediaQueueItem(DemoUtil.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(); } } diff --git a/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml b/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml new file mode 100644 index 0000000000..5f3c8961ef --- /dev/null +++ b/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml index 5d94931b64..1cce287b28 100644 --- a/demos/cast/src/main/res/layout/main_activity.xml +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -13,8 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + - + + + + + + + + + + diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml index d277bb3cdf..3505c40400 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -20,4 +20,6 @@ Cast + Add samples + 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 92e36c7f2d..1f39fe0023 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 @@ -151,7 +151,8 @@ public final class CastPlayer implements Player { * * @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}. + * the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback + * starts at position 0. * @return The Cast {@code PendingResult}, or null if no session is available. */ public PendingResult loadItem(MediaQueueItem item, long positionMs) { @@ -164,13 +165,15 @@ public final class CastPlayer implements Player { * @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}. + * the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback + * starts at position 0. * @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) { + positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; waitingForInitialTimeline = true; return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode), positionMs, null); @@ -327,6 +330,9 @@ public final class CastPlayer implements Player { @Override public void seekTo(int windowIndex, long positionMs) { MediaStatus mediaStatus = getMediaStatus(); + // We assume the default position is 0. There is no support for seeking to the default position + // in RemoteMediaClient. + positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; if (mediaStatus != null) { if (getCurrentWindowIndex() != windowIndex) { remoteMediaClient.queueJumpToItem((int) currentTimeline.getPeriod(windowIndex, period).uid, @@ -364,6 +370,7 @@ public final class CastPlayer implements Player { @Override public void stop(boolean reset) { + playbackState = STATE_IDLE; if (remoteMediaClient != null) { // TODO(b/69792021): Support or emulate stop without position reset. remoteMediaClient.stop(); @@ -450,14 +457,18 @@ public final class CastPlayer implements Player { @Override public int getNextWindowIndex() { - return C.INDEX_UNSET; + return currentTimeline.isEmpty() ? C.INDEX_UNSET + : currentTimeline.getNextWindowIndex(getCurrentWindowIndex(), repeatMode, false); } @Override public int getPreviousWindowIndex() { - return C.INDEX_UNSET; + return currentTimeline.isEmpty() ? C.INDEX_UNSET + : currentTimeline.getPreviousWindowIndex(getCurrentWindowIndex(), repeatMode, false); } + // TODO: Fill the cast timeline information with ProgressListener's duration updates. + // See [Internal: b/65152553]. @Override public long getDuration() { return currentTimeline.isEmpty() ? C.TIME_UNSET