Add queue manipulation to the Cast demo

Against all odds, samples can be reordered by using drag & drop.

Issue:#1706
Issue:#2283

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=177145553
This commit is contained in:
aquilescanta 2017-11-28 05:02:15 -08:00 committed by Oliver Woodman
parent efc709f366
commit 20567633a0
8 changed files with 517 additions and 107 deletions

View File

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

View File

@ -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<DemoUtil.Sample> {
// 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<QueueItemViewHolder> {
@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<Sample> {
public SampleListAdapter(Context context) {
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
}
}

View File

@ -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<DemoUtil.Sample> 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();
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<vector android:alpha="0.8" android:height="24dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/>
</vector>

View File

@ -13,8 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
@ -22,17 +21,28 @@
android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.SimpleExoPlayerView android:id="@+id/player_view"
android:layout_width="match_parent"
app:repeat_toggle_modes="all|one"
android:layout_height="0dp"
android:layout_weight="12" />
<ListView
android:id="@+id/sample_list"
android:choiceMode="singleChoice"
android:layout_width="match_parent"
android:layout_weight="12"
app:repeat_toggle_modes="all|one"/>
<RelativeLayout android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="12" />
<com.google.android.exoplayer2.ui.PlaybackControlView
android:id="@+id/cast_control_view"
android:layout_weight="12">
<android.support.v7.widget.RecyclerView android:id="@+id/sample_list"
android:choiceMode="singleChoice"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:fadeScrollbars="false"/>
<ImageButton android:id="@+id/add_sample_button"
android:background="@drawable/ic_add_circle_white_24dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:padding="30dp"/>
</RelativeLayout>
<com.google.android.exoplayer2.ui.PlaybackControlView android:id="@+id/cast_control_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<ListView android:id="@+id/sample_list"
android:layout_width="match_parent"
android:layout_height="250dp"
android:fadeScrollbars="false"/>
</LinearLayout>

View File

@ -20,4 +20,6 @@
<string name="media_route_menu_title">Cast</string>
<string name="sample_list_dialog_title">Add samples</string>
</resources>

View File

@ -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<MediaChannelResult> 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<MediaChannelResult> 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