/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.android.mediarouter.player; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v7.media.MediaItemStatus; import android.support.v7.media.MediaRouter.ControlRequestCallback; import android.support.v7.media.MediaRouter.RouteInfo; import android.support.v7.media.MediaSessionStatus; import android.support.v7.media.RemotePlaybackClient; import android.support.v7.media.RemotePlaybackClient.ItemActionCallback; import android.support.v7.media.RemotePlaybackClient.SessionActionCallback; import android.support.v7.media.RemotePlaybackClient.StatusCallback; import android.util.Log; import com.example.android.mediarouter.player.Player; import com.example.android.mediarouter.player.PlaylistItem; import com.example.android.mediarouter.provider.SampleMediaRouteProvider; import java.util.ArrayList; import java.util.List; /** * Handles playback of media items using a remote route. * * This class is used as a backend by PlaybackManager to feed media items to * the remote route. When the remote route doesn't support queuing, media items * are fed one-at-a-time; otherwise media items are enqueued to the remote side. */ public class RemotePlayer extends Player { private static final String TAG = "RemotePlayer"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private Context mContext; private RouteInfo mRoute; private boolean mEnqueuePending; private String mStatsInfo = ""; private List<PlaylistItem> mTempQueue = new ArrayList<PlaylistItem>(); private RemotePlaybackClient mClient; private StatusCallback mStatusCallback = new StatusCallback() { @Override public void onItemStatusChanged(Bundle data, String sessionId, MediaSessionStatus sessionStatus, String itemId, MediaItemStatus itemStatus) { logStatus("onItemStatusChanged", sessionId, sessionStatus, itemId, itemStatus); if (mCallback != null) { if (itemStatus.getPlaybackState() == MediaItemStatus.PLAYBACK_STATE_FINISHED) { mCallback.onCompletion(); } else if (itemStatus.getPlaybackState() == MediaItemStatus.PLAYBACK_STATE_ERROR) { mCallback.onError(); } } } @Override public void onSessionStatusChanged(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { logStatus("onSessionStatusChanged", sessionId, sessionStatus, null, null); if (mCallback != null) { mCallback.onPlaylistChanged(); } } @Override public void onSessionChanged(String sessionId) { if (DEBUG) { Log.d(TAG, "onSessionChanged: sessionId=" + sessionId); } } }; public RemotePlayer(Context context) { mContext = context; } @Override public boolean isRemotePlayback() { return true; } @Override public boolean isQueuingSupported() { return mClient.isQueuingSupported(); } @Override public void connect(RouteInfo route) { mRoute = route; mClient = new RemotePlaybackClient(mContext, route); mClient.setStatusCallback(mStatusCallback); if (DEBUG) { Log.d(TAG, "connected to: " + route + ", isRemotePlaybackSupported: " + mClient.isRemotePlaybackSupported() + ", isQueuingSupported: "+ mClient.isQueuingSupported()); } } @Override public void release() { mClient.release(); if (DEBUG) { Log.d(TAG, "released."); } } // basic playback operations that are always supported @Override public void play(final PlaylistItem item) { if (DEBUG) { Log.d(TAG, "play: item=" + item); } mClient.play(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() { @Override public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, String itemId, MediaItemStatus itemStatus) { logStatus("play: succeeded", sessionId, sessionStatus, itemId, itemStatus); item.setRemoteItemId(itemId); if (item.getPosition() > 0) { seekInternal(item); } if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { pause(); } if (mCallback != null) { mCallback.onPlaylistChanged(); } } @Override public void onError(String error, int code, Bundle data) { logError("play: failed", error, code); } }); } @Override public void seek(final PlaylistItem item) { seekInternal(item); } @Override public void getStatus(final PlaylistItem item, final boolean update) { if (!mClient.hasSession() || item.getRemoteItemId() == null) { // if session is not valid or item id not assigend yet. // just return, it's not fatal return; } if (DEBUG) { Log.d(TAG, "getStatus: item=" + item + ", update=" + update); } mClient.getStatus(item.getRemoteItemId(), null, new ItemActionCallback() { @Override public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, String itemId, MediaItemStatus itemStatus) { logStatus("getStatus: succeeded", sessionId, sessionStatus, itemId, itemStatus); int state = itemStatus.getPlaybackState(); if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING || state == MediaItemStatus.PLAYBACK_STATE_PAUSED || state == MediaItemStatus.PLAYBACK_STATE_PENDING) { item.setState(state); item.setPosition(itemStatus.getContentPosition()); item.setDuration(itemStatus.getContentDuration()); item.setTimestamp(itemStatus.getTimestamp()); } if (update && mCallback != null) { mCallback.onPlaylistReady(); } } @Override public void onError(String error, int code, Bundle data) { logError("getStatus: failed", error, code); if (update && mCallback != null) { mCallback.onPlaylistReady(); } } }); } @Override public void pause() { if (!mClient.hasSession()) { // ignore if no session return; } if (DEBUG) { Log.d(TAG, "pause"); } mClient.pause(null, new SessionActionCallback() { @Override public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { logStatus("pause: succeeded", sessionId, sessionStatus, null, null); if (mCallback != null) { mCallback.onPlaylistChanged(); } } @Override public void onError(String error, int code, Bundle data) { logError("pause: failed", error, code); } }); } @Override public void resume() { if (!mClient.hasSession()) { // ignore if no session return; } if (DEBUG) { Log.d(TAG, "resume"); } mClient.resume(null, new SessionActionCallback() { @Override public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { logStatus("resume: succeeded", sessionId, sessionStatus, null, null); if (mCallback != null) { mCallback.onPlaylistChanged(); } } @Override public void onError(String error, int code, Bundle data) { logError("resume: failed", error, code); } }); } @Override public void stop() { if (!mClient.hasSession()) { // ignore if no session return; } if (DEBUG) { Log.d(TAG, "stop"); } mClient.stop(null, new SessionActionCallback() { @Override public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { logStatus("stop: succeeded", sessionId, sessionStatus, null, null); if (mClient.isSessionManagementSupported()) { endSession(); } if (mCallback != null) { mCallback.onPlaylistChanged(); } } @Override public void onError(String error, int code, Bundle data) { logError("stop: failed", error, code); } }); } // enqueue & remove are only supported if isQueuingSupported() returns true @Override public void enqueue(final PlaylistItem item) { throwIfQueuingUnsupported(); if (!mClient.hasSession() && !mEnqueuePending) { mEnqueuePending = true; if (mClient.isSessionManagementSupported()) { startSession(item); } else { enqueueInternal(item); } } else if (mEnqueuePending){ mTempQueue.add(item); } else { enqueueInternal(item); } } @Override public PlaylistItem remove(String itemId) { throwIfNoSession(); throwIfQueuingUnsupported(); if (DEBUG) { Log.d(TAG, "remove: itemId=" + itemId); } mClient.remove(itemId, null, new ItemActionCallback() { @Override public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, String itemId, MediaItemStatus itemStatus) { logStatus("remove: succeeded", sessionId, sessionStatus, itemId, itemStatus); } @Override public void onError(String error, int code, Bundle data) { logError("remove: failed", error, code); } }); return null; } @Override public void updateStatistics() { // clear stats info first mStatsInfo = ""; Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_STATISTICS); intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE); if (mRoute != null && mRoute.supportsControlRequest(intent)) { ControlRequestCallback callback = new ControlRequestCallback() { @Override public void onResult(Bundle data) { if (DEBUG) { Log.d(TAG, "getStatistics: succeeded: data=" + data); } if (data != null) { int playbackCount = data.getInt( SampleMediaRouteProvider.DATA_PLAYBACK_COUNT, -1); mStatsInfo = "Total playback count: " + playbackCount; } } @Override public void onError(String error, Bundle data) { Log.d(TAG, "getStatistics: failed: error=" + error + ", data=" + data); } }; mRoute.sendControlRequest(intent, callback); } } @Override public String getStatistics() { return mStatsInfo; } private void enqueueInternal(final PlaylistItem item) { throwIfQueuingUnsupported(); if (DEBUG) { Log.d(TAG, "enqueue: item=" + item); } mClient.enqueue(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() { @Override public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, String itemId, MediaItemStatus itemStatus) { logStatus("enqueue: succeeded", sessionId, sessionStatus, itemId, itemStatus); item.setRemoteItemId(itemId); if (item.getPosition() > 0) { seekInternal(item); } if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { pause(); } if (mEnqueuePending) { mEnqueuePending = false; for (PlaylistItem item : mTempQueue) { enqueueInternal(item); } mTempQueue.clear(); } if (mCallback != null) { mCallback.onPlaylistChanged(); } } @Override public void onError(String error, int code, Bundle data) { logError("enqueue: failed", error, code); if (mCallback != null) { mCallback.onPlaylistChanged(); } } }); } private void seekInternal(final PlaylistItem item) { throwIfNoSession(); if (DEBUG) { Log.d(TAG, "seek: item=" + item); } mClient.seek(item.getRemoteItemId(), item.getPosition(), null, new ItemActionCallback() { @Override public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, String itemId, MediaItemStatus itemStatus) { logStatus("seek: succeeded", sessionId, sessionStatus, itemId, itemStatus); if (mCallback != null) { mCallback.onPlaylistChanged(); } } @Override public void onError(String error, int code, Bundle data) { logError("seek: failed", error, code); } }); } private void startSession(final PlaylistItem item) { mClient.startSession(null, new SessionActionCallback() { @Override public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { logStatus("startSession: succeeded", sessionId, sessionStatus, null, null); enqueueInternal(item); } @Override public void onError(String error, int code, Bundle data) { logError("startSession: failed", error, code); } }); } private void endSession() { mClient.endSession(null, new SessionActionCallback() { @Override public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { logStatus("endSession: succeeded", sessionId, sessionStatus, null, null); } @Override public void onError(String error, int code, Bundle data) { logError("endSession: failed", error, code); } }); } private void logStatus(String message, String sessionId, MediaSessionStatus sessionStatus, String itemId, MediaItemStatus itemStatus) { if (DEBUG) { String result = ""; if (sessionId != null && sessionStatus != null) { result += "sessionId=" + sessionId + ", sessionStatus=" + sessionStatus; } if (itemId != null & itemStatus != null) { result += (result.isEmpty() ? "" : ", ") + "itemId=" + itemId + ", itemStatus=" + itemStatus; } Log.d(TAG, message + ": " + result); } } private void logError(String message, String error, int code) { Log.d(TAG, message + ": error=" + error + ", code=" + code); } private void throwIfNoSession() { if (!mClient.hasSession()) { throw new IllegalStateException("Session is invalid"); } } private void throwIfQueuingUnsupported() { if (!isQueuingSupported()) { throw new UnsupportedOperationException("Queuing is unsupported"); } } }