/* * Copyright (C) 2015 Google Inc. All Rights Reserved. * * 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. * * ------------------------------------------------------------------------ * * Changes made by Domingos Lopes <domingos86lopes@gmail.com> * * original can be found at http://www.github.com/googlecast/CastCompanionLibrary-android */ package de.danoeh.antennapod.core.cast; import android.content.Context; import android.os.Build; import android.support.v4.view.MenuItemCompat; import android.support.v7.media.MediaRouter; import android.util.Log; import android.view.KeyEvent; import android.view.MenuItem; import com.google.android.gms.cast.ApplicationMetadata; import com.google.android.gms.cast.Cast; import com.google.android.gms.cast.CastDevice; import com.google.android.gms.cast.CastMediaControlIntent; import com.google.android.gms.cast.CastStatusCodes; 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.MediaStatus; import com.google.android.gms.cast.RemoteMediaPlayer; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager; import com.google.android.libraries.cast.companionlibrary.cast.CastConfiguration; import com.google.android.libraries.cast.companionlibrary.cast.MediaQueue; import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastException; import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException; import com.google.android.libraries.cast.companionlibrary.cast.exceptions.OnFailedListener; import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException; import org.json.JSONObject; import java.io.IOException; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.core.R; import static com.google.android.gms.cast.RemoteMediaPlayer.RESUME_STATE_PLAY; import static com.google.android.gms.cast.RemoteMediaPlayer.RESUME_STATE_UNCHANGED; /** * A subclass of {@link BaseCastManager} that is suitable for casting video contents (it * also provides a single custom data channel/namespace if an out-of-band communication is * needed). * <p> * Clients need to initialize this class by calling * {@link #init(android.content.Context)} in the Application's * {@code onCreate()} method. To access the (singleton) instance of this class, clients * need to call {@link #getInstance()}. * <p>This * class manages various states of the remote cast device. Client applications, however, can * complement the default behavior of this class by hooking into various callbacks that it provides * (see {@link CastConsumer}). * Since the number of these callbacks is usually much larger than what a single application might * be interested in, there is a no-op implementation of this interface (see * {@link DefaultCastConsumer}) that applications can subclass to override only those methods that * they are interested in. Since this library depends on the cast functionalities provided by the * Google Play services, the library checks to ensure that the right version of that service is * installed. It also provides a simple static method {@code checkGooglePlayServices()} that clients * can call at an early stage of their applications to provide a dialog for users if they need to * update/activate their Google Play Services library. * * @see CastConfiguration */ public class CastManager extends BaseCastManager implements OnFailedListener { public static final String TAG = "CastManager"; public static final String CAST_APP_ID = CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID; public static final double DEFAULT_VOLUME_STEP = 0.05; public static final long DEFAULT_LIVE_STREAM_DURATION_MS = TimeUnit.HOURS.toMillis(2); private double volumeStep = DEFAULT_VOLUME_STEP; private MediaQueue mediaQueue; private MediaStatus mediaStatus; private static CastManager INSTANCE; private RemoteMediaPlayer remoteMediaPlayer; private int state = MediaStatus.PLAYER_STATE_IDLE; private int idleReason; private final Set<CastConsumer> castConsumers = new CopyOnWriteArraySet<>(); private long liveStreamDuration = DEFAULT_LIVE_STREAM_DURATION_MS; private MediaQueueItem preLoadingItem; public static final int QUEUE_OPERATION_LOAD = 1; public static final int QUEUE_OPERATION_INSERT_ITEMS = 2; public static final int QUEUE_OPERATION_UPDATE_ITEMS = 3; public static final int QUEUE_OPERATION_JUMP = 4; public static final int QUEUE_OPERATION_REMOVE_ITEM = 5; public static final int QUEUE_OPERATION_REMOVE_ITEMS = 6; public static final int QUEUE_OPERATION_REORDER = 7; public static final int QUEUE_OPERATION_MOVE = 8; public static final int QUEUE_OPERATION_APPEND = 9; public static final int QUEUE_OPERATION_NEXT = 10; public static final int QUEUE_OPERATION_PREV = 11; public static final int QUEUE_OPERATION_SET_REPEAT = 12; private CastManager(Context context, CastConfiguration castConfiguration) { super(context, castConfiguration); Log.d(TAG, "CastManager is instantiated"); } public static synchronized CastManager init(Context context) { if (INSTANCE == null) { //TODO also setup dialog factory if necessary CastConfiguration castConfiguration = new CastConfiguration.Builder(CAST_APP_ID) .enableDebug() .enableAutoReconnect() .enableWifiReconnection() .setLaunchOptions(true, Locale.getDefault()) .build(); Log.d(TAG, "New instance of CastManager is created"); if (ConnectionResult.SUCCESS != GoogleApiAvailability.getInstance() .isGooglePlayServicesAvailable(context)) { Log.e(TAG, "Couldn't find the appropriate version of Google Play Services"); //TODO check whether creating an instance without google play services installed actually gives an exception } INSTANCE = new CastManager(context, castConfiguration); } return INSTANCE; } /** * Returns a (singleton) instance of this class. Clients should call this method in order to * get a hold of this singleton instance, only after it is initialized. If it is not initialized * yet, an {@link IllegalStateException} will be thrown. * */ public static CastManager getInstance() { if (INSTANCE == null) { String msg = "No CastManager instance was found, did you forget to initialize it?"; Log.e(TAG, msg); throw new IllegalStateException(msg); } return INSTANCE; } /** * Returns the active {@link RemoteMediaPlayer} instance. Since there are a number of media * control APIs that this library do not provide a wrapper for, client applications can call * those methods directly after obtaining an instance of the active {@link RemoteMediaPlayer}. */ public final RemoteMediaPlayer getRemoteMediaPlayer() { return remoteMediaPlayer; } /** * Determines if the media that is loaded remotely is a live stream or not. * * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public final boolean isRemoteStreamLive() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); MediaInfo info = getRemoteMediaInformation(); return (info != null) && (info.getStreamType() == MediaInfo.STREAM_TYPE_LIVE); } /* * A simple check to make sure remoteMediaPlayer is not null */ private void checkRemoteMediaPlayerAvailable() throws NoConnectionException { if (remoteMediaPlayer == null) { throw new NoConnectionException(); } } /** * Returns the url for the media that is currently playing on the remote device. If there is no * connection, this will return <code>null</code>. * * @throws NoConnectionException If no connectivity to the device exists * @throws TransientNetworkDisconnectionException If framework is still trying to recover from * a possibly transient loss of network */ public String getRemoteMediaUrl() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); if (remoteMediaPlayer != null && remoteMediaPlayer.getMediaInfo() != null) { MediaInfo info = remoteMediaPlayer.getMediaInfo(); remoteMediaPlayer.getMediaStatus().getPlayerState(); return info.getContentId(); } throw new NoConnectionException(); } /** * Indicates if the remote media is currently playing (or buffering). * * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public boolean isRemoteMediaPlaying() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); return state == MediaStatus.PLAYER_STATE_BUFFERING || state == MediaStatus.PLAYER_STATE_PLAYING; } /** * Returns <code>true</code> if the remote connected device is playing a movie. * * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public boolean isRemoteMediaPaused() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); return state == MediaStatus.PLAYER_STATE_PAUSED; } /** * Returns <code>true</code> only if there is a media on the remote being played, paused or * buffered. * * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public boolean isRemoteMediaLoaded() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); return isRemoteMediaPaused() || isRemoteMediaPlaying(); } /** * Returns the {@link MediaInfo} for the current media * * @throws NoConnectionException If no connectivity to the device exists * @throws TransientNetworkDisconnectionException If framework is still trying to recover from * a possibly transient loss of network */ public MediaInfo getRemoteMediaInformation() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); checkRemoteMediaPlayerAvailable(); return remoteMediaPlayer.getMediaInfo(); } /** * Gets the remote's system volume. It internally detects what type of volume is used. * * @throws NoConnectionException If no connectivity to the device exists * @throws TransientNetworkDisconnectionException If framework is still trying to recover from * a possibly transient loss of network */ public double getStreamVolume() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); checkRemoteMediaPlayerAvailable(); return remoteMediaPlayer.getMediaStatus().getStreamVolume(); } /** * Sets the stream volume. * * @param volume Should be a value between 0 and 1, inclusive. * @throws NoConnectionException * @throws TransientNetworkDisconnectionException * @throws CastException If setting system volume fails */ public void setStreamVolume(double volume) throws CastException, TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); if (volume > 1.0) { volume = 1.0; } else if (volume < 0) { volume = 0.0; } RemoteMediaPlayer mediaPlayer = getRemoteMediaPlayer(); if (mediaPlayer == null) { throw new NoConnectionException(); } mediaPlayer.setStreamVolume(mApiClient, volume).setResultCallback( (result) -> { if (!result.getStatus().isSuccess()) { onFailed(R.string.cast_failed_setting_volume, result.getStatus().getStatusCode()); } else { CastManager.this.onStreamVolumeChanged(); } }); } /** * Returns <code>true</code> if remote Stream is muted. * * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public boolean isStreamMute() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); checkRemoteMediaPlayerAvailable(); return remoteMediaPlayer.getMediaStatus().isMute(); } /** * Returns <code>true</code> if remote device is muted. * * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public boolean isMute() throws TransientNetworkDisconnectionException, NoConnectionException { return isStreamMute() || isDeviceMute(); } /** * Mutes or un-mutes the stream volume. * * @throws CastException * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void setStreamMute(boolean mute) throws CastException, TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); checkRemoteMediaPlayerAvailable(); remoteMediaPlayer.setStreamMute(mApiClient, mute); } /** * Returns the duration of the media that is loaded, in milliseconds. * * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public long getMediaDuration() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); checkRemoteMediaPlayerAvailable(); return remoteMediaPlayer.getStreamDuration(); } /** * Returns the time left (in milliseconds) of the current media. If there is no * {@code RemoteMediaPlayer}, it returns -1. * * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public long getMediaTimeRemaining() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); if (remoteMediaPlayer == null) { return -1; } return isRemoteStreamLive() ? liveStreamDuration : remoteMediaPlayer.getStreamDuration() - remoteMediaPlayer.getApproximateStreamPosition(); } /** * Returns the current (approximate) position of the current media, in milliseconds. * * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public long getCurrentMediaPosition() throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); checkRemoteMediaPlayerAvailable(); return remoteMediaPlayer.getApproximateStreamPosition(); } public int getApplicationStandbyState() throws IllegalStateException { Log.d(TAG, "getApplicationStandbyState()"); return Cast.CastApi.getStandbyState(mApiClient); } private void onApplicationDisconnected(int errorCode) { Log.d(TAG, "onApplicationDisconnected() reached with error code: " + errorCode); mApplicationErrorCode = errorCode; for (CastConsumer consumer : castConsumers) { consumer.onApplicationDisconnected(errorCode); } if (mMediaRouter != null) { Log.d(TAG, "onApplicationDisconnected(): Cached RouteInfo: " + getRouteInfo()); Log.d(TAG, "onApplicationDisconnected(): Selected RouteInfo: " + mMediaRouter.getSelectedRoute()); if (getRouteInfo() == null || mMediaRouter.getSelectedRoute().equals(getRouteInfo())) { Log.d(TAG, "onApplicationDisconnected(): Setting route to default"); mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute()); } } onDeviceSelected(null /* CastDevice */, null /* RouteInfo */); } private void onApplicationStatusChanged() { if (!isConnected()) { return; } try { String appStatus = Cast.CastApi.getApplicationStatus(mApiClient); Log.d(TAG, "onApplicationStatusChanged() reached: " + appStatus); for (CastConsumer consumer : castConsumers) { consumer.onApplicationStatusChanged(appStatus); } } catch (IllegalStateException e) { Log.e(TAG, "onApplicationStatusChanged()", e); } } private void onDeviceVolumeChanged() { Log.d(TAG, "onDeviceVolumeChanged() reached"); double volume; try { volume = getDeviceVolume(); boolean isMute = isDeviceMute(); for (CastConsumer consumer : castConsumers) { consumer.onVolumeChanged(volume, isMute); } } catch (TransientNetworkDisconnectionException | NoConnectionException e) { Log.e(TAG, "Failed to get volume", e); } } private void onStreamVolumeChanged() { Log.d(TAG, "onStreamVolumeChanged() reached"); double volume; try { volume = getStreamVolume(); boolean isMute = isStreamMute(); for (CastConsumer consumer : castConsumers) { consumer.onStreamVolumeChanged(volume, isMute); } } catch (TransientNetworkDisconnectionException | NoConnectionException e) { Log.e(TAG, "Failed to get volume", e); } } @Override protected void onApplicationConnected(ApplicationMetadata appMetadata, String applicationStatus, String sessionId, boolean wasLaunched) { Log.d(TAG, "onApplicationConnected() reached with sessionId: " + sessionId + ", and mReconnectionStatus=" + mReconnectionStatus); mApplicationErrorCode = NO_APPLICATION_ERROR; if (mReconnectionStatus == RECONNECTION_STATUS_IN_PROGRESS) { // we have tried to reconnect and successfully launched the app, so // it is time to select the route and make the cast icon happy :-) List<MediaRouter.RouteInfo> routes = mMediaRouter.getRoutes(); if (routes != null) { String routeId = mPreferenceAccessor.getStringFromPreference(PREFS_KEY_ROUTE_ID); for (MediaRouter.RouteInfo routeInfo : routes) { if (routeId.equals(routeInfo.getId())) { // found the right route Log.d(TAG, "Found the correct route during reconnection attempt"); mReconnectionStatus = RECONNECTION_STATUS_FINALIZED; mMediaRouter.selectRoute(routeInfo); break; } } } } try { //attachDataChannel(); attachMediaChannel(); mSessionId = sessionId; // saving device for future retrieval; we only save the last session info mPreferenceAccessor.saveStringToPreference(PREFS_KEY_SESSION_ID, mSessionId); remoteMediaPlayer.requestStatus(mApiClient).setResultCallback(result -> { if (!result.getStatus().isSuccess()) { onFailed(R.string.cast_failed_status_request, result.getStatus().getStatusCode()); } }); for (CastConsumer consumer : castConsumers) { consumer.onApplicationConnected(appMetadata, mSessionId, wasLaunched); } } catch (TransientNetworkDisconnectionException e) { Log.e(TAG, "Failed to attach media/data channel due to network issues", e); onFailed(R.string.cast_failed_no_connection_trans, NO_STATUS_CODE); } catch (NoConnectionException e) { Log.e(TAG, "Failed to attach media/data channel due to network issues", e); onFailed(R.string.cast_failed_no_connection, NO_STATUS_CODE); } } /* * (non-Javadoc) * @see com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager * #onConnectivityRecovered() */ @Override public void onConnectivityRecovered() { reattachMediaChannel(); //reattachDataChannel(); super.onConnectivityRecovered(); } /* * (non-Javadoc) * @see com.google.android.gms.cast.CastClient.Listener#onApplicationStopFailed (int) */ @Override public void onApplicationStopFailed(int errorCode) { for (CastConsumer consumer : castConsumers) { consumer.onApplicationStopFailed(errorCode); } } @Override public void onApplicationConnectionFailed(int errorCode) { Log.d(TAG, "onApplicationConnectionFailed() reached with errorCode: " + errorCode); mApplicationErrorCode = errorCode; if (mReconnectionStatus == RECONNECTION_STATUS_IN_PROGRESS) { if (errorCode == CastStatusCodes.APPLICATION_NOT_RUNNING) { // while trying to re-establish session, we found out that the app is not running // so we need to disconnect mReconnectionStatus = RECONNECTION_STATUS_INACTIVE; onDeviceSelected(null /* CastDevice */, null /* RouteInfo */); } } else { for (CastConsumer consumer : castConsumers) { consumer.onApplicationConnectionFailed(errorCode); } onDeviceSelected(null /* CastDevice */, null /* RouteInfo */); if (mMediaRouter != null) { Log.d(TAG, "onApplicationConnectionFailed(): Setting route to default"); mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute()); } } } /** * Loads a media. For this to succeed, you need to have successfully launched the application. * * @param media The media to be loaded * @param autoPlay If <code>true</code>, playback starts after load * @param position Where to start the playback (only used if autoPlay is <code>true</code>. * Units is milliseconds. * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void loadMedia(MediaInfo media, boolean autoPlay, int position) throws TransientNetworkDisconnectionException, NoConnectionException { loadMedia(media, autoPlay, position, null); } /** * Loads a media. For this to succeed, you need to have successfully launched the application. * * @param media The media to be loaded * @param autoPlay If <code>true</code>, playback starts after load * @param position Where to start the playback (only used if autoPlay is <code>true</code>). * Units is milliseconds. * @param customData Optional {@link JSONObject} data to be passed to the cast device * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void loadMedia(MediaInfo media, boolean autoPlay, int position, JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { loadMedia(media, null, autoPlay, position, customData); } /** * Loads a media. For this to succeed, you need to have successfully launched the application. * * @param media The media to be loaded * @param activeTracks An array containing the list of track IDs to be set active for this * media upon a successful load * @param autoPlay If <code>true</code>, playback starts after load * @param position Where to start the playback (only used if autoPlay is <code>true</code>). * Units is milliseconds. * @param customData Optional {@link JSONObject} data to be passed to the cast device * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void loadMedia(MediaInfo media, final long[] activeTracks, boolean autoPlay, int position, JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "loadMedia"); checkConnectivity(); if (media == null) { return; } if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to load a video with no active media session"); throw new NoConnectionException(); } Log.d(TAG, "remoteMediaPlayer.load() with media=" + media.getMetadata().getString(MediaMetadata.KEY_TITLE) + ", position=" + position + ", autoplay=" + autoPlay); remoteMediaPlayer.load(mApiClient, media, autoPlay, position, activeTracks, customData) .setResultCallback(result -> { for (CastConsumer consumer : castConsumers) { consumer.onMediaLoadResult(result.getStatus().getStatusCode()); } }); } /** * Loads and optionally starts playback of a new queue of media items. * * @param items Array of items to load, in the order that they should be played. Must not be * {@code null} or empty. * @param startIndex The array index of the item in the {@code items} array that should be * played first (i.e., it will become the currentItem).If {@code repeatMode} * is {@link MediaStatus#REPEAT_MODE_REPEAT_OFF} playback will end when the * last item in the array is played. * <p> * This may be useful for continuation scenarios where the user was already * using the sender application and in the middle decides to cast. This lets * the sender application avoid mapping between the local and remote queue * positions and/or avoid issuing an extra request to update the queue. * <p> * This value must be less than the length of {@code items}. * @param repeatMode The repeat playback mode for the queue. One of * {@link MediaStatus#REPEAT_MODE_REPEAT_OFF}, * {@link MediaStatus#REPEAT_MODE_REPEAT_ALL}, * {@link MediaStatus#REPEAT_MODE_REPEAT_SINGLE} and * {@link MediaStatus#REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE}. * @param customData Custom application-specific data to pass along with the request, may be * {@code null}. * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public void queueLoad(final MediaQueueItem[] items, final int startIndex, final int repeatMode, final JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "queueLoad"); checkConnectivity(); if (items == null || items.length == 0) { return; } if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to queue one or more videos with no active media session"); throw new NoConnectionException(); } Log.d(TAG, "remoteMediaPlayer.queueLoad() with " + items.length + "items, starting at " + startIndex); remoteMediaPlayer .queueLoad(mApiClient, items, startIndex, repeatMode, customData) .setResultCallback(result -> { for (CastConsumer consumer : castConsumers) { consumer.onMediaQueueOperationResult(QUEUE_OPERATION_LOAD, result.getStatus().getStatusCode()); } }); } /** * Inserts a list of new media items into the queue. * * @param itemsToInsert List of items to insert into the queue, in the order that they should be * played. The itemId field of the items should be unassigned or the * request will fail with an INVALID_PARAMS error. Must not be {@code null} * or empty. * @param insertBeforeItemId ID of the item that will be located immediately after the inserted * list. If the value is {@link MediaQueueItem#INVALID_ITEM_ID} or * invalid, the inserted list will be appended to the end of the * queue. * @param customData Custom application-specific data to pass along with the request. May be * {@code null}. * @throws TransientNetworkDisconnectionException * @throws NoConnectionException * @throws IllegalArgumentException */ public void queueInsertItems(final MediaQueueItem[] itemsToInsert, final int insertBeforeItemId, final JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "queueInsertItems"); checkConnectivity(); if (itemsToInsert == null || itemsToInsert.length == 0) { throw new IllegalArgumentException("items cannot be empty or null"); } if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to insert into queue with no active media session"); throw new NoConnectionException(); } remoteMediaPlayer .queueInsertItems(mApiClient, itemsToInsert, insertBeforeItemId, customData) .setResultCallback( result -> { for (CastConsumer consumer : castConsumers) { consumer.onMediaQueueOperationResult( QUEUE_OPERATION_INSERT_ITEMS, result.getStatus().getStatusCode()); } }); } /** * Updates properties of a subset of the existing items in the media queue. * * @param itemsToUpdate List of queue items to be updated. The items will retain the existing * order and will be fully replaced with the ones provided, including the * media information. Any other items currently in the queue will remain * unchanged. The tracks information can not change once the item is loaded * (if the item is the currentItem). If any of the items does not exist it * will be ignored. * @param customData Custom application-specific data to pass along with the request. May be * {@code null}. * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public void queueUpdateItems(final MediaQueueItem[] itemsToUpdate, final JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to update the queue with no active media session"); throw new NoConnectionException(); } remoteMediaPlayer .queueUpdateItems(mApiClient, itemsToUpdate, customData).setResultCallback( result -> { Log.d(TAG, "queueUpdateItems() " + result.getStatus() + result.getStatus() .isSuccess()); for (CastConsumer consumer : castConsumers) { consumer.onMediaQueueOperationResult(QUEUE_OPERATION_UPDATE_ITEMS, result.getStatus().getStatusCode()); } }); } /** * Plays the item with {@code itemId} in the queue. * <p> * If {@code itemId} is not found in the queue, this method will report success without sending * a request to the receiver. * * @param itemId The ID of the item to which to jump. * @param customData Custom application-specific data to pass along with the request. May be * {@code null}. * @throws TransientNetworkDisconnectionException * @throws NoConnectionException * @throws IllegalArgumentException */ public void queueJumpToItem(int itemId, final JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException, IllegalArgumentException { checkConnectivity(); if (itemId == MediaQueueItem.INVALID_ITEM_ID) { throw new IllegalArgumentException("itemId is not valid"); } if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to jump in a queue with no active media session"); throw new NoConnectionException(); } remoteMediaPlayer .queueJumpToItem(mApiClient, itemId, customData).setResultCallback( result -> { for (CastConsumer consumer : castConsumers) { consumer.onMediaQueueOperationResult(QUEUE_OPERATION_JUMP, result.getStatus().getStatusCode()); } }); } /** * Removes a list of items from the queue. If the remaining queue is empty, the media session * will be terminated. * * @param itemIdsToRemove The list of media item IDs to remove. Must not be {@code null} or * empty. * @param customData Custom application-specific data to pass along with the request. May be * {@code null}. * @throws TransientNetworkDisconnectionException * @throws NoConnectionException * @throws IllegalArgumentException */ public void queueRemoveItems(final int[] itemIdsToRemove, final JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException, IllegalArgumentException { Log.d(TAG, "queueRemoveItems"); checkConnectivity(); if (itemIdsToRemove == null || itemIdsToRemove.length == 0) { throw new IllegalArgumentException("itemIds cannot be empty or null"); } if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to remove items from queue with no active media session"); throw new NoConnectionException(); } remoteMediaPlayer .queueRemoveItems(mApiClient, itemIdsToRemove, customData).setResultCallback( result -> { for (CastConsumer consumer : castConsumers) { consumer.onMediaQueueOperationResult(QUEUE_OPERATION_REMOVE_ITEMS, result.getStatus().getStatusCode()); } }); } /** * Removes the item with {@code itemId} from the queue. * <p> * If {@code itemId} is not found in the queue, this method will silently return without sending * a request to the receiver. A {@code itemId} may not be in the queue because it wasn't * originally in the queue, or it was removed by another sender. * * @param itemId The ID of the item to be removed. * @param customData Custom application-specific data to pass along with the request. May be * {@code null}. * @throws TransientNetworkDisconnectionException * @throws NoConnectionException * @throws IllegalArgumentException */ public void queueRemoveItem(final int itemId, final JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException, IllegalArgumentException { Log.d(TAG, "queueRemoveItem"); checkConnectivity(); if (itemId == MediaQueueItem.INVALID_ITEM_ID) { throw new IllegalArgumentException("itemId is invalid"); } if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to remove an item from queue with no active media session"); throw new NoConnectionException(); } remoteMediaPlayer .queueRemoveItem(mApiClient, itemId, customData).setResultCallback( result -> { for (CastConsumer consumer : castConsumers) { consumer.onMediaQueueOperationResult(QUEUE_OPERATION_REMOVE_ITEM, result.getStatus().getStatusCode()); } }); } /** * Reorder a list of media items in the queue. * * @param itemIdsToReorder The list of media item IDs to reorder, in the new order. Any other * items currently in the queue will maintain their existing order. The * list will be inserted just before the item specified by * {@code insertBeforeItemId}, or at the end of the queue if * {@code insertBeforeItemId} is {@link MediaQueueItem#INVALID_ITEM_ID}. * <p> * For example: * <p> * If insertBeforeItemId is not specified <br> * Existing queue: "A","D","G","H","B","E" <br> * itemIds: "D","H","B" <br> * New Order: "A","G","E","D","H","B" <br> * <p> * If insertBeforeItemId is "A" <br> * Existing queue: "A","D","G","H","B" <br> * itemIds: "D","H","B" <br> * New Order: "D","H","B","A","G","E" <br> * <p> * If insertBeforeItemId is "G" <br> * Existing queue: "A","D","G","H","B" <br> * itemIds: "D","H","B" <br> * New Order: "A","D","H","B","G","E" <br> * <p> * If any of the items does not exist it will be ignored. * Must not be {@code null} or empty. * @param insertBeforeItemId ID of the item that will be located immediately after the reordered * list. If set to {@link MediaQueueItem#INVALID_ITEM_ID}, the * reordered list will be appended at the end of the queue. * @param customData Custom application-specific data to pass along with the request. May be * {@code null}. * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public void queueReorderItems(final int[] itemIdsToReorder, final int insertBeforeItemId, final JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException, IllegalArgumentException { Log.d(TAG, "queueReorderItems"); checkConnectivity(); if (itemIdsToReorder == null || itemIdsToReorder.length == 0) { throw new IllegalArgumentException("itemIdsToReorder cannot be empty or null"); } if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to reorder items in a queue with no active media session"); throw new NoConnectionException(); } remoteMediaPlayer .queueReorderItems(mApiClient, itemIdsToReorder, insertBeforeItemId, customData) .setResultCallback( result -> { for (CastConsumer consumer : castConsumers) { consumer.onMediaQueueOperationResult(QUEUE_OPERATION_REORDER, result.getStatus().getStatusCode()); } }); } /** * Moves the item with {@code itemId} to a new position in the queue. * <p> * If {@code itemId} is not found in the queue, either because it wasn't there originally or it * was removed by another sender before calling this function, this function will silently * return without sending a request to the receiver. * * @param itemId The ID of the item to be moved. * @param newIndex The new index of the item. If the value is negative, an error will be * returned. If the value is out of bounds, or becomes out of bounds because the * queue was shortened by another sender while this request is in progress, the * item will be moved to the end of the queue. * @param customData Custom application-specific data to pass along with the request. May be * {@code null}. * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public void queueMoveItemToNewIndex(int itemId, int newIndex, final JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "queueMoveItemToNewIndex"); checkConnectivity(); if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to mote item to new index with no active media session"); throw new NoConnectionException(); } remoteMediaPlayer .queueMoveItemToNewIndex(mApiClient, itemId, newIndex, customData) .setResultCallback( result -> { for (CastConsumer consumer : castConsumers) { consumer.onMediaQueueOperationResult(QUEUE_OPERATION_MOVE, result.getStatus().getStatusCode()); } }); } /** * Appends a new media item to the end of the queue. * * @param item The item to append. Must not be {@code null}. * @param customData Custom application-specific data to pass along with the request. May be * {@code null}. * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public void queueAppendItem(MediaQueueItem item, final JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "queueAppendItem"); checkConnectivity(); if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to append item with no active media session"); throw new NoConnectionException(); } remoteMediaPlayer .queueAppendItem(mApiClient, item, customData) .setResultCallback( result -> { for (CastConsumer consumer : castConsumers) { consumer.onMediaQueueOperationResult(QUEUE_OPERATION_APPEND, result.getStatus().getStatusCode()); } }); } /** * Jumps to the next item in the queue. * * @param customData Custom application-specific data to pass along with the request. May be * {@code null}. * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public void queueNext(final JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "queueNext"); checkConnectivity(); if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to update the queue with no active media session"); throw new NoConnectionException(); } remoteMediaPlayer .queueNext(mApiClient, customData).setResultCallback( result -> { for (CastConsumer consumer : castConsumers) { consumer.onMediaQueueOperationResult(QUEUE_OPERATION_NEXT, result.getStatus().getStatusCode()); } }); } /** * Jumps to the previous item in the queue. * * @param customData Custom application-specific data to pass along with the request. May be * {@code null}. * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public void queuePrev(final JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "queuePrev"); checkConnectivity(); if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to update the queue with no active media session"); throw new NoConnectionException(); } remoteMediaPlayer .queuePrev(mApiClient, customData).setResultCallback( result -> { for (CastConsumer consumer : castConsumers) { consumer.onMediaQueueOperationResult(QUEUE_OPERATION_PREV, result.getStatus().getStatusCode()); } }); } /** * Inserts an item in the queue and starts the playback of that newly inserted item. It is * assumed that we are inserting before the "current item" * * @param item The item to be inserted * @param insertBeforeItemId ID of the item that will be located immediately after the inserted * and is assumed to be the "current item" * @param customData Custom application-specific data to pass along with the request. May be * {@code null}. * @throws TransientNetworkDisconnectionException * @throws NoConnectionException * @throws IllegalArgumentException */ public void queueInsertBeforeCurrentAndPlay(MediaQueueItem item, int insertBeforeItemId, final JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "queueInsertBeforeCurrentAndPlay"); checkConnectivity(); if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to insert into queue with no active media session"); throw new NoConnectionException(); } if (item == null || insertBeforeItemId == MediaQueueItem.INVALID_ITEM_ID) { throw new IllegalArgumentException( "item cannot be empty or insertBeforeItemId cannot be invalid"); } remoteMediaPlayer.queueInsertItems(mApiClient, new MediaQueueItem[]{item}, insertBeforeItemId, customData).setResultCallback( result -> { if (result.getStatus().isSuccess()) { try { queuePrev(customData); } catch (TransientNetworkDisconnectionException | NoConnectionException e) { Log.e(TAG, "queuePrev() Failed to skip to previous", e); } } for (CastConsumer consumer : castConsumers) { consumer.onMediaQueueOperationResult(QUEUE_OPERATION_INSERT_ITEMS, result.getStatus().getStatusCode()); } }); } /** * Sets the repeat mode of the queue. * * @param repeatMode The repeat playback mode for the queue. * @param customData Custom application-specific data to pass along with the request. May be * {@code null}. * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public void queueSetRepeatMode(final int repeatMode, final JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "queueSetRepeatMode"); checkConnectivity(); if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to update the queue with no active media session"); throw new NoConnectionException(); } remoteMediaPlayer .queueSetRepeatMode(mApiClient, repeatMode, customData).setResultCallback( result -> { if (!result.getStatus().isSuccess()) { Log.d(TAG, "Failed with status: " + result.getStatus()); } for (CastConsumer consumer : castConsumers) { consumer.onMediaQueueOperationResult(QUEUE_OPERATION_SET_REPEAT, result.getStatus().getStatusCode()); } }); } /** * Plays the loaded media. * * @param position Where to start the playback. Units is milliseconds. * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void play(int position) throws TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); Log.d(TAG, "attempting to play media at position " + position + " seconds"); if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to play a video with no active media session"); throw new NoConnectionException(); } seekAndPlay(position); } /** * Resumes the playback from where it was left (can be the beginning). * * @param customData Optional {@link JSONObject} data to be passed to the cast device * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void play(JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "play(customData)"); checkConnectivity(); if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to play a video with no active media session"); throw new NoConnectionException(); } remoteMediaPlayer.play(mApiClient, customData) .setResultCallback(result -> { if (!result.getStatus().isSuccess()) { onFailed(R.string.cast_failed_to_play, result.getStatus().getStatusCode()); } }); } /** * Resumes the playback from where it was left (can be the beginning). * * @throws CastException * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void play() throws CastException, TransientNetworkDisconnectionException, NoConnectionException { play(null); } /** * Stops the playback of media/stream * * @param customData Optional {@link JSONObject} * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public void stop(JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "stop()"); checkConnectivity(); if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to stop a stream with no active media session"); throw new NoConnectionException(); } remoteMediaPlayer.stop(mApiClient, customData).setResultCallback( result -> { if (!result.getStatus().isSuccess()) { onFailed(R.string.cast_failed_to_stop, result.getStatus().getStatusCode()); } } ); } /** * Stops the playback of media/stream * * @throws CastException * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public void stop() throws CastException, TransientNetworkDisconnectionException, NoConnectionException { stop(null); } /** * Pauses the playback. * * @throws CastException * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void pause() throws CastException, TransientNetworkDisconnectionException, NoConnectionException { pause(null); } /** * Pauses the playback. * * @param customData Optional {@link JSONObject} data to be passed to the cast device * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void pause(JSONObject customData) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "attempting to pause media"); checkConnectivity(); if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to pause a video with no active media session"); throw new NoConnectionException(); } remoteMediaPlayer.pause(mApiClient, customData) .setResultCallback(result -> { if (!result.getStatus().isSuccess()) { onFailed(R.string.cast_failed_to_pause, result.getStatus().getStatusCode()); } }); } /** * Seeks to the given point without changing the state of the player, i.e. after seek is * completed, it resumes what it was doing before the start of seek. * * @param position in milliseconds * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void seek(int position) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "attempting to seek media"); checkConnectivity(); if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to seek a video with no active media session"); throw new NoConnectionException(); } Log.d(TAG, "remoteMediaPlayer.seek() to position " + position); remoteMediaPlayer.seek(mApiClient, position, RESUME_STATE_UNCHANGED). setResultCallback(result -> { if (!result.getStatus().isSuccess()) { onFailed(R.string.cast_failed_seek, result.getStatus().getStatusCode()); } }); } /** * Fast forwards the media by the given amount. If {@code lengthInMillis} is negative, it * rewinds the media. * * @param lengthInMillis The amount to fast forward the media, given in milliseconds * @throws TransientNetworkDisconnectionException * @throws NoConnectionException */ public void forward(int lengthInMillis) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "forward(): attempting to forward media by " + lengthInMillis); checkConnectivity(); if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to seek a video with no active media session"); throw new NoConnectionException(); } long position = remoteMediaPlayer.getApproximateStreamPosition() + lengthInMillis; seek((int) position); } /** * Seeks to the given point and starts playback regardless of the starting state. * * @param position in milliseconds * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void seekAndPlay(int position) throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "attempting to seek media"); checkConnectivity(); if (remoteMediaPlayer == null) { Log.e(TAG, "Trying to seekAndPlay a video with no active media session"); throw new NoConnectionException(); } Log.d(TAG, "remoteMediaPlayer.seek() to position " + position + "and play"); remoteMediaPlayer.seek(mApiClient, position, RESUME_STATE_PLAY) .setResultCallback(result -> { if (!result.getStatus().isSuccess()) { onFailed(R.string.cast_failed_seek, result.getStatus().getStatusCode()); } }); } /** * Toggles the playback of the media. * * @throws CastException * @throws NoConnectionException * @throws TransientNetworkDisconnectionException */ public void togglePlayback() throws CastException, TransientNetworkDisconnectionException, NoConnectionException { checkConnectivity(); boolean isPlaying = isRemoteMediaPlaying(); if (isPlaying) { pause(); } else { if (state == MediaStatus.PLAYER_STATE_IDLE && idleReason == MediaStatus.IDLE_REASON_FINISHED) { loadMedia(getRemoteMediaInformation(), true, 0); } else { play(); } } } private void attachMediaChannel() throws TransientNetworkDisconnectionException, NoConnectionException { Log.d(TAG, "attachMediaChannel()"); checkConnectivity(); if (remoteMediaPlayer == null) { remoteMediaPlayer = new RemoteMediaPlayer(); remoteMediaPlayer.setOnStatusUpdatedListener( () -> { Log.d(TAG, "RemoteMediaPlayer::onStatusUpdated() is reached"); CastManager.this.onRemoteMediaPlayerStatusUpdated(); } ); remoteMediaPlayer.setOnPreloadStatusUpdatedListener( () -> { Log.d(TAG, "RemoteMediaPlayer::onPreloadStatusUpdated() is reached"); CastManager.this.onRemoteMediaPreloadStatusUpdated(); }); remoteMediaPlayer.setOnMetadataUpdatedListener( () -> { Log.d(TAG, "RemoteMediaPlayer::onMetadataUpdated() is reached"); CastManager.this.onRemoteMediaPlayerMetadataUpdated(); } ); remoteMediaPlayer.setOnQueueStatusUpdatedListener( () -> { Log.d(TAG, "RemoteMediaPlayer::onQueueStatusUpdated() is reached"); mediaStatus = remoteMediaPlayer.getMediaStatus(); if (mediaStatus != null && mediaStatus.getQueueItems() != null) { List<MediaQueueItem> queueItems = mediaStatus .getQueueItems(); int itemId = mediaStatus.getCurrentItemId(); MediaQueueItem item = mediaStatus .getQueueItemById(itemId); int repeatMode = mediaStatus.getQueueRepeatMode(); onQueueUpdated(queueItems, item, repeatMode, false); } else { onQueueUpdated(null, null, MediaStatus.REPEAT_MODE_REPEAT_OFF, false); } }); } try { Log.d(TAG, "Registering MediaChannel namespace"); Cast.CastApi.setMessageReceivedCallbacks(mApiClient, remoteMediaPlayer.getNamespace(), remoteMediaPlayer); } catch (IOException | IllegalStateException e) { Log.e(TAG, "attachMediaChannel()", e); } } private void reattachMediaChannel() { if (remoteMediaPlayer != null && mApiClient != null) { try { Log.d(TAG, "Registering MediaChannel namespace"); Cast.CastApi.setMessageReceivedCallbacks(mApiClient, remoteMediaPlayer.getNamespace(), remoteMediaPlayer); } catch (IOException | IllegalStateException e) { Log.e(TAG, "reattachMediaChannel()", e); } } } private void detachMediaChannel() { Log.d(TAG, "trying to detach media channel"); if (remoteMediaPlayer != null) { try { Cast.CastApi.removeMessageReceivedCallbacks(mApiClient, remoteMediaPlayer.getNamespace()); } catch (IOException | IllegalStateException e) { Log.e(TAG, "detachMediaChannel()", e); } remoteMediaPlayer = null; } } /** * Returns the playback status of the remote device. * * @return Returns one of the values * <ul> * <li> <code>MediaStatus.PLAYER_STATE_UNKNOWN</code></li> * <li> <code>MediaStatus.PLAYER_STATE_IDLE</code></li> * <li> <code>MediaStatus.PLAYER_STATE_PLAYING</code></li> * <li> <code>MediaStatus.PLAYER_STATE_PAUSED</code></li> * <li> <code>MediaStatus.PLAYER_STATE_BUFFERING</code></li> * </ul> */ public int getPlaybackStatus() { return state; } /** * Returns the latest retrieved value for the {@link MediaStatus}. This value is updated * whenever the onStatusUpdated callback is called. */ public final MediaStatus getMediaStatus() { return mediaStatus; } /** * Returns the Idle reason, defined in <code>MediaStatus.IDLE_*</code>. Note that the returned * value is only meaningful if the status is truly <code>MediaStatus.PLAYER_STATE_IDLE * </code> * * <p>Possible values are: * <ul> * <li>IDLE_REASON_NONE</li> * <li>IDLE_REASON_FINISHED</li> * <li>IDLE_REASON_CANCELED</li> * <li>IDLE_REASON_INTERRUPTED</li> * <li>IDLE_REASON_ERROR</li> * </ul> */ public int getIdleReason() { return idleReason; } private void onMessageSendFailed(int errorCode) { for (CastConsumer consumer : castConsumers) { consumer.onDataMessageSendFailed(errorCode); } } /* * This is called by onStatusUpdated() of the RemoteMediaPlayer */ private void onRemoteMediaPlayerStatusUpdated() { Log.d(TAG, "onRemoteMediaPlayerStatusUpdated() reached"); if (mApiClient == null || remoteMediaPlayer == null) { Log.d(TAG, "mApiClient or remoteMediaPlayer is null, so will not proceed"); return; } mediaStatus = remoteMediaPlayer.getMediaStatus(); if (mediaStatus == null) { Log.d(TAG, "MediaStatus is null, so will not proceed"); return; } else { List<MediaQueueItem> queueItems = mediaStatus.getQueueItems(); if (queueItems != null) { int itemId = mediaStatus.getCurrentItemId(); MediaQueueItem item = mediaStatus.getQueueItemById(itemId); int repeatMode = mediaStatus.getQueueRepeatMode(); onQueueUpdated(queueItems, item, repeatMode, false); } else { onQueueUpdated(null, null, MediaStatus.REPEAT_MODE_REPEAT_OFF, false); } state = mediaStatus.getPlayerState(); idleReason = mediaStatus.getIdleReason(); if (state == MediaStatus.PLAYER_STATE_PLAYING) { Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = playing"); } else if (state == MediaStatus.PLAYER_STATE_PAUSED) { Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = paused"); } else if (state == MediaStatus.PLAYER_STATE_IDLE) { Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = IDLE with reason: " + idleReason); if (idleReason == MediaStatus.IDLE_REASON_ERROR) { // something bad happened on the cast device Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): IDLE reason = ERROR"); onFailed(R.string.cast_failed_receiver_player_error, NO_STATUS_CODE); } } else if (state == MediaStatus.PLAYER_STATE_BUFFERING) { Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = buffering"); } else { Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = unknown"); } } for (CastConsumer consumer : castConsumers) { consumer.onRemoteMediaPlayerStatusUpdated(); } if (mediaStatus != null) { double volume = mediaStatus.getStreamVolume(); boolean isMute = mediaStatus.isMute(); for (CastConsumer consumer : castConsumers) { consumer.onStreamVolumeChanged(volume, isMute); } } } private void onRemoteMediaPreloadStatusUpdated() { MediaQueueItem item = null; mediaStatus = remoteMediaPlayer.getMediaStatus(); if (mediaStatus != null) { item = mediaStatus.getQueueItemById(mediaStatus.getPreloadedItemId()); } preLoadingItem = item; Log.d(TAG, "onRemoteMediaPreloadStatusUpdated() " + item); for (CastConsumer consumer : castConsumers) { consumer.onRemoteMediaPreloadStatusUpdated(item); } } public MediaQueueItem getPreLoadingItem() { return preLoadingItem; } /* * This is called by onQueueStatusUpdated() of RemoteMediaPlayer */ private void onQueueUpdated(List<MediaQueueItem> queueItems, MediaQueueItem item, int repeatMode, boolean shuffle) { Log.d(TAG, "onQueueUpdated() reached"); Log.d(TAG, String.format("Queue Items size: %d, Item: %s, Repeat Mode: %d, Shuffle: %s", queueItems == null ? 0 : queueItems.size(), item, repeatMode, shuffle)); if (queueItems != null) { mediaQueue = new MediaQueue(new CopyOnWriteArrayList<>(queueItems), item, shuffle, repeatMode); } else { mediaQueue = new MediaQueue(new CopyOnWriteArrayList<>(), null, false, MediaStatus.REPEAT_MODE_REPEAT_OFF); } for (CastConsumer consumer : castConsumers) { consumer.onMediaQueueUpdated(queueItems, item, repeatMode, shuffle); } } /* * This is called by onMetadataUpdated() of RemoteMediaPlayer */ public void onRemoteMediaPlayerMetadataUpdated() { Log.d(TAG, "onRemoteMediaPlayerMetadataUpdated() reached"); for (CastConsumer consumer : castConsumers) { consumer.onRemoteMediaPlayerMetadataUpdated(); } } /** * Registers a {@link CastConsumer} interface with this class. * Registered listeners will be notified of changes to a variety of * lifecycle and media status changes through the callbacks that the interface provides. * * @see DefaultCastConsumer */ public synchronized void addCastConsumer(CastConsumer listener) { if (listener != null) { addBaseCastConsumer(listener); castConsumers.add(listener); Log.d(TAG, "Successfully added the new CastConsumer listener " + listener); } } /** * Unregisters a {@link CastConsumer}. */ public synchronized void removeCastConsumer(CastConsumer listener) { if (listener != null) { removeBaseCastConsumer(listener); castConsumers.remove(listener); } } @Override protected void onDeviceUnselected() { detachMediaChannel(); //removeDataChannel(); state = MediaStatus.PLAYER_STATE_IDLE; mediaStatus = null; } @Override protected Cast.CastOptions.Builder getCastOptionBuilder(CastDevice device) { Cast.CastOptions.Builder builder = new Cast.CastOptions.Builder(mSelectedCastDevice, new CastListener()); if (isFeatureEnabled(CastConfiguration.FEATURE_DEBUGGING)) { builder.setVerboseLoggingEnabled(true); } return builder; } @Override public void onConnectionFailed(ConnectionResult result) { super.onConnectionFailed(result); state = MediaStatus.PLAYER_STATE_IDLE; mediaStatus = null; } @Override public void onDisconnected(boolean stopAppOnExit, boolean clearPersistedConnectionData, boolean setDefaultRoute) { super.onDisconnected(stopAppOnExit, clearPersistedConnectionData, setDefaultRoute); state = MediaStatus.PLAYER_STATE_IDLE; mediaStatus = null; mediaQueue = null; } class CastListener extends Cast.Listener { /* * (non-Javadoc) * @see com.google.android.gms.cast.Cast.Listener#onApplicationDisconnected (int) */ @Override public void onApplicationDisconnected(int statusCode) { CastManager.this.onApplicationDisconnected(statusCode); } /* * (non-Javadoc) * @see com.google.android.gms.cast.Cast.Listener#onApplicationStatusChanged () */ @Override public void onApplicationStatusChanged() { CastManager.this.onApplicationStatusChanged(); } @Override public void onVolumeChanged() { CastManager.this.onDeviceVolumeChanged(); } } @Override public void onFailed(int resourceId, int statusCode) { Log.d(TAG, "onFailed: " + mContext.getString(resourceId) + ", code: " + statusCode); super.onFailed(resourceId, statusCode); } /** * Clients can call this method to delegate handling of the volume. Clients should override * {@code dispatchEvent} and call this method: * <pre> public boolean dispatchKeyEvent(KeyEvent event) { if (mCastManager.onDispatchVolumeKeyEvent(event, VOLUME_DELTA)) { return true; } return super.dispatchKeyEvent(event); } * </pre> * @param event The dispatched event. * @param volumeDelta The amount by which volume should be increased or decreased in each step * @return <code>true</code> if volume is handled by the library, <code>false</code> otherwise. */ public boolean onDispatchVolumeKeyEvent(KeyEvent event, double volumeDelta) { if (isConnected()) { boolean isKeyDown = event.getAction() == KeyEvent.ACTION_DOWN; switch (event.getKeyCode()) { case KeyEvent.KEYCODE_VOLUME_UP: return changeVolume(volumeDelta, isKeyDown); case KeyEvent.KEYCODE_VOLUME_DOWN: return changeVolume(-volumeDelta, isKeyDown); } } return false; } private boolean changeVolume(double volumeIncrement, boolean isKeyDown) { if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) && getPlaybackStatus() == MediaStatus.PLAYER_STATE_PLAYING && isFeatureEnabled(CastConfiguration.FEATURE_LOCKSCREEN)) { return false; } if (isKeyDown) { try { adjustDeviceVolume(volumeIncrement); } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) { Log.e(TAG, "Failed to change volume", e); } } return true; } /** * Sets the volume step, i.e. the fraction by which volume will increase or decrease each time * user presses the hard volume buttons on the device. * * @param volumeStep Should be a double between 0 and 1, inclusive. */ public CastManager setVolumeStep(double volumeStep) { if ((volumeStep > 1) || (volumeStep < 0)) { throw new IllegalArgumentException("Volume Step should be between 0 and 1, inclusive"); } this.volumeStep = volumeStep; return this; } /** * Returns the volume step. The default value is {@code DEFAULT_VOLUME_STEP}. */ public double getVolumeStep() { return volumeStep; } public final MediaQueue getMediaQueue() { return mediaQueue; } /** * Checks whether the selected Cast Device has the specified audio or video capabilities. * * @param capability capability from: * <ul> * <li>{@link CastDevice#CAPABILITY_AUDIO_IN}</li> * <li>{@link CastDevice#CAPABILITY_AUDIO_OUT}</li> * <li>{@link CastDevice#CAPABILITY_VIDEO_IN}</li> * <li>{@link CastDevice#CAPABILITY_VIDEO_OUT}</li> * </ul> * @param defaultVal value to return whenever there's no device selected. * @return {@code true} if the selected device has the specified capability, * {@code false} otherwise. */ public boolean hasCapability(final int capability, final boolean defaultVal) { if (mSelectedCastDevice != null) { return mSelectedCastDevice.hasCapability(capability); } else { return defaultVal; } } /** * Adds and wires up the Switchable Media Router cast button. It returns a reference to the * {@link SwitchableMediaRouteActionProvider} associated with the button if the caller needs * such reference. It is assumed that the enclosing * {@link android.app.Activity} inherits (directly or indirectly) from * {@link android.support.v7.app.AppCompatActivity}. * * @param menuItem MenuItem of the Media Router cast button. */ public final SwitchableMediaRouteActionProvider addMediaRouterButton(MenuItem menuItem) { SwitchableMediaRouteActionProvider mediaRouteActionProvider = (SwitchableMediaRouteActionProvider) MenuItemCompat.getActionProvider(menuItem); mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector); if (mCastConfiguration.getMediaRouteDialogFactory() != null) { mediaRouteActionProvider.setDialogFactory(mCastConfiguration.getMediaRouteDialogFactory()); } return mediaRouteActionProvider; } /* (non-Javadoc) * These methods startReconnectionService and stopReconnectionService simply override the ones * from BaseCastManager with empty implementations because we handle the service ourselves, but * need to allow BaseCastManager to save current network information. */ @Override protected void startReconnectionService(long mediaDurationLeft) { // Do nothing } @Override protected void stopReconnectionService() { // Do nothing } }