Chromium Code Reviews| 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); |
| + } |
| + } |
| +} |