1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.mediarouter.player; 18 19 import android.content.Context; 20 import android.content.Intent; 21 import android.os.Bundle; 22 import android.support.v7.media.MediaItemStatus; 23 import android.support.v7.media.MediaRouter.ControlRequestCallback; 24 import android.support.v7.media.MediaRouter.RouteInfo; 25 import android.support.v7.media.MediaSessionStatus; 26 import android.support.v7.media.RemotePlaybackClient; 27 import android.support.v7.media.RemotePlaybackClient.ItemActionCallback; 28 import android.support.v7.media.RemotePlaybackClient.SessionActionCallback; 29 import android.support.v7.media.RemotePlaybackClient.StatusCallback; 30 import android.util.Log; 31 32 import com.example.android.mediarouter.player.Player; 33 import com.example.android.mediarouter.player.PlaylistItem; 34 import com.example.android.mediarouter.provider.SampleMediaRouteProvider; 35 36 import java.util.ArrayList; 37 import java.util.List; 38 39 /** 40 * Handles playback of media items using a remote route. 41 * 42 * This class is used as a backend by PlaybackManager to feed media items to 43 * the remote route. When the remote route doesn't support queuing, media items 44 * are fed one-at-a-time; otherwise media items are enqueued to the remote side. 45 */ 46 public class RemotePlayer extends Player { 47 private static final String TAG = "RemotePlayer"; 48 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 49 private Context mContext; 50 private RouteInfo mRoute; 51 private boolean mEnqueuePending; 52 private String mStatsInfo = ""; 53 private List<PlaylistItem> mTempQueue = new ArrayList<PlaylistItem>(); 54 55 private RemotePlaybackClient mClient; 56 private StatusCallback mStatusCallback = new StatusCallback() { 57 @Override 58 public void onItemStatusChanged(Bundle data, 59 String sessionId, MediaSessionStatus sessionStatus, 60 String itemId, MediaItemStatus itemStatus) { 61 logStatus("onItemStatusChanged", sessionId, sessionStatus, itemId, itemStatus); 62 if (mCallback != null) { 63 if (itemStatus.getPlaybackState() == 64 MediaItemStatus.PLAYBACK_STATE_FINISHED) { 65 mCallback.onCompletion(); 66 } else if (itemStatus.getPlaybackState() == 67 MediaItemStatus.PLAYBACK_STATE_ERROR) { 68 mCallback.onError(); 69 } 70 } 71 } 72 73 @Override 74 public void onSessionStatusChanged(Bundle data, 75 String sessionId, MediaSessionStatus sessionStatus) { 76 logStatus("onSessionStatusChanged", sessionId, sessionStatus, null, null); 77 if (mCallback != null) { 78 mCallback.onPlaylistChanged(); 79 } 80 } 81 82 @Override 83 public void onSessionChanged(String sessionId) { 84 if (DEBUG) { 85 Log.d(TAG, "onSessionChanged: sessionId=" + sessionId); 86 } 87 } 88 }; 89 90 public RemotePlayer(Context context) { 91 mContext = context; 92 } 93 94 @Override 95 public boolean isRemotePlayback() { 96 return true; 97 } 98 99 @Override 100 public boolean isQueuingSupported() { 101 return mClient.isQueuingSupported(); 102 } 103 104 @Override 105 public void connect(RouteInfo route) { 106 mRoute = route; 107 mClient = new RemotePlaybackClient(mContext, route); 108 mClient.setStatusCallback(mStatusCallback); 109 110 if (DEBUG) { 111 Log.d(TAG, "connected to: " + route 112 + ", isRemotePlaybackSupported: " + mClient.isRemotePlaybackSupported() 113 + ", isQueuingSupported: "+ mClient.isQueuingSupported()); 114 } 115 } 116 117 @Override 118 public void release() { 119 mClient.release(); 120 121 if (DEBUG) { 122 Log.d(TAG, "released."); 123 } 124 } 125 126 // basic playback operations that are always supported 127 @Override 128 public void play(final PlaylistItem item) { 129 if (DEBUG) { 130 Log.d(TAG, "play: item=" + item); 131 } 132 mClient.play(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() { 133 @Override 134 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 135 String itemId, MediaItemStatus itemStatus) { 136 logStatus("play: succeeded", sessionId, sessionStatus, itemId, itemStatus); 137 item.setRemoteItemId(itemId); 138 if (item.getPosition() > 0) { 139 seekInternal(item); 140 } 141 if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { 142 pause(); 143 } 144 if (mCallback != null) { 145 mCallback.onPlaylistChanged(); 146 } 147 } 148 149 @Override 150 public void onError(String error, int code, Bundle data) { 151 logError("play: failed", error, code); 152 } 153 }); 154 } 155 156 @Override 157 public void seek(final PlaylistItem item) { 158 seekInternal(item); 159 } 160 161 @Override 162 public void getStatus(final PlaylistItem item, final boolean update) { 163 if (!mClient.hasSession() || item.getRemoteItemId() == null) { 164 // if session is not valid or item id not assigend yet. 165 // just return, it's not fatal 166 return; 167 } 168 169 if (DEBUG) { 170 Log.d(TAG, "getStatus: item=" + item + ", update=" + update); 171 } 172 mClient.getStatus(item.getRemoteItemId(), null, new ItemActionCallback() { 173 @Override 174 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 175 String itemId, MediaItemStatus itemStatus) { 176 logStatus("getStatus: succeeded", sessionId, sessionStatus, itemId, itemStatus); 177 int state = itemStatus.getPlaybackState(); 178 if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING 179 || state == MediaItemStatus.PLAYBACK_STATE_PAUSED 180 || state == MediaItemStatus.PLAYBACK_STATE_PENDING) { 181 item.setState(state); 182 item.setPosition(itemStatus.getContentPosition()); 183 item.setDuration(itemStatus.getContentDuration()); 184 item.setTimestamp(itemStatus.getTimestamp()); 185 } 186 if (update && mCallback != null) { 187 mCallback.onPlaylistReady(); 188 } 189 } 190 191 @Override 192 public void onError(String error, int code, Bundle data) { 193 logError("getStatus: failed", error, code); 194 if (update && mCallback != null) { 195 mCallback.onPlaylistReady(); 196 } 197 } 198 }); 199 } 200 201 @Override 202 public void pause() { 203 if (!mClient.hasSession()) { 204 // ignore if no session 205 return; 206 } 207 if (DEBUG) { 208 Log.d(TAG, "pause"); 209 } 210 mClient.pause(null, new SessionActionCallback() { 211 @Override 212 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 213 logStatus("pause: succeeded", sessionId, sessionStatus, null, null); 214 if (mCallback != null) { 215 mCallback.onPlaylistChanged(); 216 } 217 } 218 219 @Override 220 public void onError(String error, int code, Bundle data) { 221 logError("pause: failed", error, code); 222 } 223 }); 224 } 225 226 @Override 227 public void resume() { 228 if (!mClient.hasSession()) { 229 // ignore if no session 230 return; 231 } 232 if (DEBUG) { 233 Log.d(TAG, "resume"); 234 } 235 mClient.resume(null, new SessionActionCallback() { 236 @Override 237 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 238 logStatus("resume: succeeded", sessionId, sessionStatus, null, null); 239 if (mCallback != null) { 240 mCallback.onPlaylistChanged(); 241 } 242 } 243 244 @Override 245 public void onError(String error, int code, Bundle data) { 246 logError("resume: failed", error, code); 247 } 248 }); 249 } 250 251 @Override 252 public void stop() { 253 if (!mClient.hasSession()) { 254 // ignore if no session 255 return; 256 } 257 if (DEBUG) { 258 Log.d(TAG, "stop"); 259 } 260 mClient.stop(null, new SessionActionCallback() { 261 @Override 262 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 263 logStatus("stop: succeeded", sessionId, sessionStatus, null, null); 264 if (mClient.isSessionManagementSupported()) { 265 endSession(); 266 } 267 if (mCallback != null) { 268 mCallback.onPlaylistChanged(); 269 } 270 } 271 272 @Override 273 public void onError(String error, int code, Bundle data) { 274 logError("stop: failed", error, code); 275 } 276 }); 277 } 278 279 // enqueue & remove are only supported if isQueuingSupported() returns true 280 @Override 281 public void enqueue(final PlaylistItem item) { 282 throwIfQueuingUnsupported(); 283 284 if (!mClient.hasSession() && !mEnqueuePending) { 285 mEnqueuePending = true; 286 if (mClient.isSessionManagementSupported()) { 287 startSession(item); 288 } else { 289 enqueueInternal(item); 290 } 291 } else if (mEnqueuePending){ 292 mTempQueue.add(item); 293 } else { 294 enqueueInternal(item); 295 } 296 } 297 298 @Override 299 public PlaylistItem remove(String itemId) { 300 throwIfNoSession(); 301 throwIfQueuingUnsupported(); 302 303 if (DEBUG) { 304 Log.d(TAG, "remove: itemId=" + itemId); 305 } 306 mClient.remove(itemId, null, new ItemActionCallback() { 307 @Override 308 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 309 String itemId, MediaItemStatus itemStatus) { 310 logStatus("remove: succeeded", sessionId, sessionStatus, itemId, itemStatus); 311 } 312 313 @Override 314 public void onError(String error, int code, Bundle data) { 315 logError("remove: failed", error, code); 316 } 317 }); 318 319 return null; 320 } 321 322 @Override 323 public void updateStatistics() { 324 // clear stats info first 325 mStatsInfo = ""; 326 327 Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_STATISTICS); 328 intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE); 329 330 if (mRoute != null && mRoute.supportsControlRequest(intent)) { 331 ControlRequestCallback callback = new ControlRequestCallback() { 332 @Override 333 public void onResult(Bundle data) { 334 if (DEBUG) { 335 Log.d(TAG, "getStatistics: succeeded: data=" + data); 336 } 337 if (data != null) { 338 int playbackCount = data.getInt( 339 SampleMediaRouteProvider.DATA_PLAYBACK_COUNT, -1); 340 mStatsInfo = "Total playback count: " + playbackCount; 341 } 342 } 343 344 @Override 345 public void onError(String error, Bundle data) { 346 Log.d(TAG, "getStatistics: failed: error=" + error + ", data=" + data); 347 } 348 }; 349 350 mRoute.sendControlRequest(intent, callback); 351 } 352 } 353 354 @Override 355 public String getStatistics() { 356 return mStatsInfo; 357 } 358 359 private void enqueueInternal(final PlaylistItem item) { 360 throwIfQueuingUnsupported(); 361 362 if (DEBUG) { 363 Log.d(TAG, "enqueue: item=" + item); 364 } 365 mClient.enqueue(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() { 366 @Override 367 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 368 String itemId, MediaItemStatus itemStatus) { 369 logStatus("enqueue: succeeded", sessionId, sessionStatus, itemId, itemStatus); 370 item.setRemoteItemId(itemId); 371 if (item.getPosition() > 0) { 372 seekInternal(item); 373 } 374 if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { 375 pause(); 376 } 377 if (mEnqueuePending) { 378 mEnqueuePending = false; 379 for (PlaylistItem item : mTempQueue) { 380 enqueueInternal(item); 381 } 382 mTempQueue.clear(); 383 } 384 if (mCallback != null) { 385 mCallback.onPlaylistChanged(); 386 } 387 } 388 389 @Override 390 public void onError(String error, int code, Bundle data) { 391 logError("enqueue: failed", error, code); 392 if (mCallback != null) { 393 mCallback.onPlaylistChanged(); 394 } 395 } 396 }); 397 } 398 399 private void seekInternal(final PlaylistItem item) { 400 throwIfNoSession(); 401 402 if (DEBUG) { 403 Log.d(TAG, "seek: item=" + item); 404 } 405 mClient.seek(item.getRemoteItemId(), item.getPosition(), null, new ItemActionCallback() { 406 @Override 407 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 408 String itemId, MediaItemStatus itemStatus) { 409 logStatus("seek: succeeded", sessionId, sessionStatus, itemId, itemStatus); 410 if (mCallback != null) { 411 mCallback.onPlaylistChanged(); 412 } 413 } 414 415 @Override 416 public void onError(String error, int code, Bundle data) { 417 logError("seek: failed", error, code); 418 } 419 }); 420 } 421 422 private void startSession(final PlaylistItem item) { 423 mClient.startSession(null, new SessionActionCallback() { 424 @Override 425 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 426 logStatus("startSession: succeeded", sessionId, sessionStatus, null, null); 427 enqueueInternal(item); 428 } 429 430 @Override 431 public void onError(String error, int code, Bundle data) { 432 logError("startSession: failed", error, code); 433 } 434 }); 435 } 436 437 private void endSession() { 438 mClient.endSession(null, new SessionActionCallback() { 439 @Override 440 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 441 logStatus("endSession: succeeded", sessionId, sessionStatus, null, null); 442 } 443 444 @Override 445 public void onError(String error, int code, Bundle data) { 446 logError("endSession: failed", error, code); 447 } 448 }); 449 } 450 451 private void logStatus(String message, 452 String sessionId, MediaSessionStatus sessionStatus, 453 String itemId, MediaItemStatus itemStatus) { 454 if (DEBUG) { 455 String result = ""; 456 if (sessionId != null && sessionStatus != null) { 457 result += "sessionId=" + sessionId + ", sessionStatus=" + sessionStatus; 458 } 459 if (itemId != null & itemStatus != null) { 460 result += (result.isEmpty() ? "" : ", ") 461 + "itemId=" + itemId + ", itemStatus=" + itemStatus; 462 } 463 Log.d(TAG, message + ": " + result); 464 } 465 } 466 467 private void logError(String message, String error, int code) { 468 Log.d(TAG, message + ": error=" + error + ", code=" + code); 469 } 470 471 private void throwIfNoSession() { 472 if (!mClient.hasSession()) { 473 throw new IllegalStateException("Session is invalid"); 474 } 475 } 476 477 private void throwIfQueuingUnsupported() { 478 if (!isQueuingSupported()) { 479 throw new UnsupportedOperationException("Queuing is unsupported"); 480 } 481 } 482 }