Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1982)

Unified Diff: chrome/android/java/src/org/chromium/chrome/browser/media/remote/AbstractMediaRouteController.java

Issue 928643003: Upstream Chrome for Android Cast. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 5 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/android/java/src/org/chromium/chrome/browser/media/remote/AbstractMediaRouteController.java
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/AbstractMediaRouteController.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/AbstractMediaRouteController.java
new file mode 100644
index 0000000000000000000000000000000000000000..bfd7ba03cb56af1a4b98e546cc730990bd2203e2
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/AbstractMediaRouteController.java
@@ -0,0 +1,576 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.media.remote;
+
+import android.content.Context;
+import android.os.Handler;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaItemStatus;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaRouter;
+import android.support.v7.media.MediaRouter.RouteInfo;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.google.android.gms.cast.CastMediaControlIntent;
+
+import org.chromium.base.ApplicationStatus;
+import org.chromium.base.CommandLine;
+import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
+import org.chromium.chrome.ChromeSwitches;
+import org.chromium.chrome.R;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import javax.annotation.Nullable;
+
+/**
+ * Class containing the common, connection type independent, code for all MediaRouteControllers.
+ */
+public abstract class AbstractMediaRouteController implements MediaRouteController {
+
+ /**
+ * Callback class for monitoring whether any routes exist, and hence deciding whether to show
+ * the cast UI to users.
+ */
+ private class DeviceDiscoveryCallback extends MediaRouter.Callback {
+ @Override
+ public void onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider) {
+ updateRouteAvailability();
+ }
+
+ @Override
+ public void onProviderChanged(
+ MediaRouter router, MediaRouter.ProviderInfo provider) {
+ updateRouteAvailability();
+ }
+
+ @Override
+ public void onProviderRemoved(
+ MediaRouter router, MediaRouter.ProviderInfo provider) {
+ updateRouteAvailability();
+ }
+
+ @Override
+ public void onRouteAdded(MediaRouter router, RouteInfo route) {
+ if (mDebug) Log.d(TAG, "Added route " + route.getName() + " " + route.getId());
+ updateRouteAvailability();
+ }
+
+ @Override
+ public void onRouteRemoved(MediaRouter router, RouteInfo route) {
+ if (mDebug) {
+ Log.d(TAG, "Removed route " + route.getName() + " " + route.getId());
+ }
+ updateRouteAvailability();
+ }
+
+ private void updateRouteAvailability() {
+ if (mediaRouterInitializationFailed()) return;
+
+ boolean routesAvailable = getMediaRouter().isRouteAvailable(mMediaRouteSelector,
+ MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE);
+ if (routesAvailable != mRoutesAvailable) {
+ mRoutesAvailable = routesAvailable;
+ if (mDebug)
+ Log.d(TAG,
+ "Remote media route availability changed, updating listeners");
+ for (MediaStateListener listener : mAvailableRouteListeners) {
+ listener.onRouteAvailabilityChanged(routesAvailable);
+ }
+ }
+ }
+ }
+
+ /**
+ * Callback class for monitoring whether a route has been selected, and the state of the
+ * selected route.
+ */
+ private class DeviceSelectionCallback extends MediaRouter.Callback {
+ private Runnable mConnectionFailureNotifier = new Runnable() {
+ @Override
+ public void run() {
+ release();
+ mConnectionFailureNotifierQueued = false;
+ }
+ };
+
+ /** True if we are waiting for the MediaRouter route to connect or reconnect */
+ private boolean mConnectionFailureNotifierQueued = false;
+
+ public void clearConnectionFailureCallback() {
+ getHandler().removeCallbacks(mConnectionFailureNotifier);
+ mConnectionFailureNotifierQueued = false;
+ }
+
+ @Override
+ public void onRouteAdded(MediaRouter router, RouteInfo route) {
+ onRouteAddedEvent(router, route);
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, RouteInfo route) {
+ // We only care about changes to the current route.
+ if (!route.equals(getCurrentRoute())) return;
+ // When there is no wifi connection, this condition becomes true.
+ if (route.isConnecting()) {
+ // We don't want to post the same Runnable twice.
+ if (!mConnectionFailureNotifierQueued) {
+ mConnectionFailureNotifierQueued = true;
+ getHandler().postDelayed(mConnectionFailureNotifier,
+ CONNECTION_FAILURE_NOTIFICATION_DELAY_MS);
+ }
+ } else {
+ // Only cancel the disconnect if we already posted the message. We can get into this
+ // situation if we swap the current route provider (for example, switching to a YT
+ // video while casting a non-YT video).
+ if (mConnectionFailureNotifierQueued) {
+ // We have reconnected, cancel the delayed disconnect.
+ getHandler().removeCallbacks(mConnectionFailureNotifier);
+ mConnectionFailureNotifierQueued = false;
+ }
+ }
+ }
+
+ @Override
+ public void onRouteSelected(MediaRouter router, RouteInfo route) {
+ onRouteSelectedEvent(router, route);
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouter router, RouteInfo route) {
+ onRouteUnselectedEvent(router, route);
+ }
+ }
+
+ /** Number of ms to wait for reconnection, after which we call the failure callbacks. */
+ protected static final int CONNECTION_FAILURE_NOTIFICATION_DELAY_MS = 10000;
+ private static final int END_OF_VIDEO_THRESHOLD_MS = 500;
+ private static final String TAG = "AbstractMediaRouteController";
+ private final Set<MediaStateListener> mAvailableRouteListeners;
+ private final Context mContext;
+ private RouteInfo mCurrentRoute;
+ private boolean mDebug;
+ private final DeviceDiscoveryCallback mDeviceDiscoveryCallback;;
+ private String mDeviceId;
+ private final DeviceSelectionCallback mDeviceSelectionCallback;
+
+ private final Handler mHandler;
+ private boolean mIsPrepared = false;
+
+ private final MediaRouter mMediaRouter;
+
+ private final MediaRouteSelector mMediaRouteSelector;
+ private MediaStateListener mMediaStateListener;
+ private PlayerState mPlaybackState = PlayerState.FINISHED;
+ private boolean mRoutesAvailable = false;
+ private final Set<UiListener> mUiListeners;
+ private boolean mWatchingRouteSelection = false;
+ /**
whywhat 2015/02/25 16:31:31 nit: Lost an empty line before the comment?
aberent 2015/03/11 18:29:57 Done.
+ * Sole constructor
+ */
+ protected AbstractMediaRouteController() {
+
+ mDebug = CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_CAST_DEBUG_LOGS);
+
+ mContext = ApplicationStatus.getApplicationContext();
+ assert (getContext() != null);
+
+ mHandler = new Handler();
+
+ mMediaRouteSelector = buildMediaRouteSelector();
+
+ MediaRouter mediaRouter;
+
+ try {
+ // Pre-MR1 versions of JB do not have the complete MediaRouter APIs,
+ // so getting the MediaRouter instance will throw an exception.
+ mediaRouter = MediaRouter.getInstance(getContext());
+ } catch (NoSuchMethodError e) {
+ Log.e(TAG, "Can't get an instance of MediaRouter, casting is not supported."
+ + " Are you still on JB (JVP15S)?");
+ mediaRouter = null;
+ }
+ mMediaRouter = mediaRouter;
+
+ mAvailableRouteListeners = new HashSet<MediaStateListener>();
+ // TODO(aberent): I am unclear why this is accessed from multiple threads, but
+ // if I make it a HashSet then it gets ConcurrentModificationExceptions on some
+ // types of disconnect. Investigate and fix.
+ mUiListeners = new CopyOnWriteArraySet<UiListener>();
+
+ mDeviceDiscoveryCallback = new DeviceDiscoveryCallback();
+ mDeviceSelectionCallback = new DeviceSelectionCallback();
+ }
+
+ @Override
+ public void addRouteAvailabilityListener(MediaStateListener listener) {
+ if (mediaRouterInitializationFailed()) return;
+ if (mAvailableRouteListeners.isEmpty()) {
whywhat 2015/02/25 16:31:31 nit: also an empty line between the ifs?
aberent 2015/03/11 18:29:57 Done.
+ getMediaRouter().addCallback(mMediaRouteSelector, mDeviceDiscoveryCallback,
+ MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
+ if (mDebug) Log.d(TAG, "Started device discovery");
+
+ // Get the initial state
+ mRoutesAvailable = getMediaRouter().isRouteAvailable(
+ mMediaRouteSelector, MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE);
+ }
+ mAvailableRouteListeners.add(listener);
+ // Send the current state to the listener.
+ listener.onRouteAvailabilityChanged(mRoutesAvailable);
+ }
+
+ @Override
+ public void addUiListener(UiListener listener) {
+ mUiListeners.add(listener);
+ }
+
+ protected void clearConnectionFailureCallback() {
+ mDeviceSelectionCallback.clearConnectionFailureCallback();
+ }
+
+ /**
+ * Clear the current playing item (if any) but not the associated session.
+ */
+ protected void clearItemState() {
+ mPlaybackState = PlayerState.FINISHED;
+ updateTitle(null);
+ }
+
+ /**
+ * Reset the media route to the default
+ */
+ protected void clearMediaRoute() {
+ if (getMediaRouter() != null) {
+ getMediaRouter().getDefaultRoute().select();
+ registerRoute(getMediaRouter().getDefaultRoute());
+ RemotePlaybackSettings.setDeviceId(getContext(), null);
+ }
+ }
+
+ @Override
+ public boolean currentRouteSupportsRemotePlayback() {
+ return mCurrentRoute != null && mCurrentRoute.supportsControlCategory(
+ MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ }
+
+ protected final Context getContext() {
+ return mContext;
+ }
+
+ protected final RouteInfo getCurrentRoute() {
+ return mCurrentRoute;
+ }
+
+ protected final String getDeviceId() {
+ return mDeviceId;
+ }
+
+ protected final Handler getHandler() {
+ return mHandler;
+ }
+
+ /**
+ * @return the mMediaRouter
+ */
+ protected final MediaRouter getMediaRouter() {
+ return mMediaRouter;
+ }
+
+ @Override
+ public final MediaStateListener getMediaStateListener() {
+ return mMediaStateListener;
+ }
+
+ @Override
+ public final PlayerState getPlayerState() {
+ return mPlaybackState;
+ }
+
+ @Override
+ public final String getRouteName() {
+ return mCurrentRoute == null ? null : mCurrentRoute.getName();
+ }
+
+ /**
+ * @return The list of MediaRouteController.Listener objects that will receive messages from
+ * this class.
+ */
+ protected final Set<UiListener> getUiListeners() {
+ return mUiListeners;
+ }
+
+ private final boolean isAtEndOfVideo(int positionMs, int videoLengthMs) {
+ return videoLengthMs - positionMs < END_OF_VIDEO_THRESHOLD_MS && videoLengthMs > 0;
+ }
+
+ @Override
+ public final boolean isBeingCast() {
+ return (mPlaybackState != PlayerState.INVALIDATED && mPlaybackState != PlayerState.ERROR
+ && mPlaybackState != PlayerState.FINISHED);
+ }
+
+ @Override
+ public final boolean isPlaying() {
+ return mPlaybackState == PlayerState.PLAYING || mPlaybackState == PlayerState.LOADING;
+ }
+
+ @Override
+ public final boolean isRemotePlaybackAvailable() {
+ if (mediaRouterInitializationFailed()) return false;
+
+ return getMediaRouter().getSelectedRoute().getPlaybackType()
+ == MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE || getMediaRouter().isRouteAvailable(
+ mMediaRouteSelector, MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE);
+ }
+
+ protected final boolean mediaRouterInitializationFailed() {
+ return getMediaRouter() == null;
+ }
+
+ protected final void notifyRouteSelected(RouteInfo route) {
+ for (UiListener listener : mUiListeners) {
+ listener.onRouteSelected(route.getName(), this);
+ }
+ if (mMediaStateListener != null) mMediaStateListener.onRouteSelected(route.getName());
+ }
+
+ /**
+ * DisconnectListener implementation
+ */
+ @Override
+ public void onDisconnect() {
+ RecordCastAction.castEndedTimeRemaining(getDuration(), getDuration() - getPosition());
+ release();
+ }
+
+ @Override
+ public void onPause() {
+ pause();
+ }
+
+ @Override
+ public void onPlay() {
+ resume();
+ }
+
+ protected abstract void onRouteAddedEvent(MediaRouter router, RouteInfo route);
+
+ protected abstract void onRouteSelectedEvent(MediaRouter router, RouteInfo route);
+
+ protected abstract void onRouteUnselectedEvent(MediaRouter router, RouteInfo route);
+
+ @Override
+ public void onSeek(int position) {
+ seekTo(position);
+ }
+
+ @Override
+ public void onStop() {
+ release();
+ }
+
+ @Override
+ public void prepareMediaRoute() {
+ startWatchingRouteSelection();
+ }
+
+ protected final void registerRoute(RouteInfo route) {
+ mCurrentRoute = route;
+
+ if (route != null) {
+ setDeviceId(route.getId());
+ if (mDebug) Log.d(TAG, "Selected route " + getDeviceId());
+ if (!route.isDefault()) {
+ RemotePlaybackSettings.setDeviceId(getContext(), getDeviceId());
+ }
+ } else {
+ RemotePlaybackSettings.setDeviceId(getContext(), null);
+ }
+ }
+
+ protected void removeAllListeners() {
+ mUiListeners.clear();
+ }
+
+ @Override
+ public void removeRouteAvailabilityListener(MediaStateListener listener) {
+ if (mediaRouterInitializationFailed()) return;
+
+ mAvailableRouteListeners.remove(listener);
+ if (mAvailableRouteListeners.isEmpty()) {
+ getMediaRouter().removeCallback(mDeviceDiscoveryCallback);
+ if (mDebug) Log.d(TAG, "Stopped device discovery");
+ }
+ }
+
+ @Override
+ public void removeUiListener(UiListener listener) {
+ mUiListeners.remove(listener);
+ }
+
+ @Override
+ public boolean routeIsDefaultRoute() {
+ return mCurrentRoute != null && mCurrentRoute.isDefault();
+ }
+
+ protected void sendErrorToListeners(int error) {
+ String errorMessage =
+ getContext().getString(R.string.cast_error_playing_video, mCurrentRoute.getName());
+
+ for (UiListener listener : mUiListeners) {
+ listener.onError(error, errorMessage);
+ }
+
+ if (mMediaStateListener != null) mMediaStateListener.onError();
+ }
+
+ protected void setDeviceId(String mDeviceId) {
+ this.mDeviceId = mDeviceId;
+ }
+
+ @Override
+ public void setMediaStateListener(MediaStateListener mediaStateListener) {
+ mMediaStateListener = mediaStateListener;
+ }
+
+ private void setPrepared() {
+ if (!mIsPrepared) {
+ for (UiListener listener : mUiListeners) {
+ listener.onPrepared(this);
+ }
+ if (mMediaStateListener != null) mMediaStateListener.onPrepared();
+ RecordCastAction.castDefaultPlayerResult(true);
+ mIsPrepared = true;
+ }
+ }
+
+ protected void setUnprepared() {
+ mIsPrepared = false;
+ }
+
+ @Override
+ public boolean shouldResetState(MediaStateListener newPlayer) {
+ return !isBeingCast() || newPlayer != getMediaStateListener();
+ }
+
+ protected void showCastError(String routeName) {
+ Toast toast = Toast.makeText(
+ getContext(),
+ getContext().getString(R.string.cast_error_playing_video, routeName),
+ Toast.LENGTH_SHORT);
+ toast.show();
+ }
+
+ private void startWatchingRouteSelection() {
+ if (mWatchingRouteSelection || mediaRouterInitializationFailed()) return;
+
+ mWatchingRouteSelection = true;
+ // Start listening
+ getMediaRouter().addCallback(mMediaRouteSelector, mDeviceSelectionCallback,
+ MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
+ if (mDebug) Log.d(TAG, "Started route selection discovery");
+ }
+
+ protected void stopWatchingRouteSelection() {
+ mWatchingRouteSelection = false;
+ if (getMediaRouter() != null) {
+ getMediaRouter().removeCallback(mDeviceSelectionCallback);
+ if (mDebug) Log.d(TAG, "Stopped route selection discovery");
+ }
+ }
+
+ protected void updateState(int state) {
+ if (mDebug) {
+ Log.d(TAG, "updateState oldState: " + this.mPlaybackState + " newState: " + state);
+ }
+
+ PlayerState oldState = this.mPlaybackState;
+
+ PlayerState playerState = PlayerState.STOPPED;
+ switch (state) {
+ case MediaItemStatus.PLAYBACK_STATE_BUFFERING:
+ playerState = PlayerState.LOADING;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_CANCELED:
+ playerState = PlayerState.FINISHED;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_ERROR:
+ playerState = PlayerState.ERROR;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_FINISHED:
+ playerState = PlayerState.FINISHED;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_INVALIDATED:
+ playerState = PlayerState.INVALIDATED;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_PAUSED:
+ if (isAtEndOfVideo(getPosition(), getDuration())) {
+ playerState = PlayerState.FINISHED;
+ } else {
+ playerState = PlayerState.PAUSED;
+ }
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_PENDING:
+ playerState = PlayerState.PAUSED;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_PLAYING:
+ playerState = PlayerState.PLAYING;
+ break;
+ default:
+ break;
+ }
+
+ this.mPlaybackState = playerState;
+
+ for (UiListener listener : mUiListeners) {
+ listener.onPlaybackStateChanged(oldState, playerState);
+ }
+
+ if (mMediaStateListener != null) mMediaStateListener.onPlaybackStateChanged(playerState);
+
+ if (oldState != mPlaybackState) {
+ // We need to persist our state in case we get killed.
+ RemotePlaybackSettings.setLastVideoState(getContext(), mPlaybackState.name());
+
+ switch (mPlaybackState) {
+ case PLAYING:
+ RemotePlaybackSettings.setRemainingTime(getContext(),
+ getDuration() - getPosition());
+ RemotePlaybackSettings.setLastPlayedTime(getContext(),
+ System.currentTimeMillis());
+ RemotePlaybackSettings.setShouldReconnectToRemote(getContext(),
+ !mCurrentRoute.isDefault());
+ setPrepared();
+ break;
+ case PAUSED:
+ RemotePlaybackSettings.setShouldReconnectToRemote(getContext(),
+ !mCurrentRoute.isDefault());
+ setPrepared();
+ break;
+ case FINISHED:
+ release();
+ break;
+ case INVALIDATED:
+ clearItemState();
+ break;
+ case ERROR:
+ sendErrorToListeners(CastMediaControlIntent.ERROR_CODE_REQUEST_FAILED);
+ release();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ protected void updateTitle(@Nullable String newTitle) {
+ for (UiListener listener : mUiListeners) {
+ listener.onTitleChanged(newTitle);
+ }
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698