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:
parent
efc709f366
commit
20567633a0
@ -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
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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>
|
@ -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"
|
||||
|
25
demos/cast/src/main/res/layout/sample_list.xml
Normal file
25
demos/cast/src/main/res/layout/sample_list.xml
Normal 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>
|
@ -20,4 +20,6 @@
|
||||
|
||||
<string name="media_route_menu_title">Cast</string>
|
||||
|
||||
<string name="sample_list_dialog_title">Add samples</string>
|
||||
|
||||
</resources>
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user