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.supportv7.media; 18 19 import android.content.Context; 20 import android.content.Intent; 21 import android.graphics.Bitmap; 22 import android.os.Bundle; 23 import android.support.v7.media.MediaItemStatus; 24 import android.support.v7.media.MediaRouter.ControlRequestCallback; 25 import android.support.v7.media.MediaRouter.RouteInfo; 26 import android.support.v7.media.MediaSessionStatus; 27 import android.support.v7.media.RemotePlaybackClient; 28 import android.support.v7.media.RemotePlaybackClient.ItemActionCallback; 29 import android.support.v7.media.RemotePlaybackClient.SessionActionCallback; 30 import android.support.v7.media.RemotePlaybackClient.StatusCallback; 31 import android.util.Log; 32 33 import java.util.ArrayList; 34 import java.util.List; 35 36 /** 37 * Handles playback of media items using a remote route. 38 * 39 * This class is used as a backend by PlaybackManager to feed media items to 40 * the remote route. When the remote route doesn't support queuing, media items 41 * are fed one-at-a-time; otherwise media items are enqueued to the remote side. 42 */ 43 public class RemotePlayer extends Player { 44 private static final String TAG = "RemotePlayer"; 45 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 46 private Context mContext; 47 private RouteInfo mRoute; 48 private boolean mEnqueuePending; 49 private String mTrackInfo = ""; 50 private Bitmap mSnapshot; 51 private List<PlaylistItem> mTempQueue = new ArrayList<PlaylistItem>(); 52 53 private RemotePlaybackClient mClient; 54 private StatusCallback mStatusCallback = new StatusCallback() { 55 @Override 56 public void onItemStatusChanged(Bundle data, 57 String sessionId, MediaSessionStatus sessionStatus, 58 String itemId, MediaItemStatus itemStatus) { 59 logStatus("onItemStatusChanged", sessionId, sessionStatus, itemId, itemStatus); 60 if (mCallback != null) { 61 if (itemStatus.getPlaybackState() == 62 MediaItemStatus.PLAYBACK_STATE_FINISHED) { 63 mCallback.onCompletion(); 64 } else if (itemStatus.getPlaybackState() == 65 MediaItemStatus.PLAYBACK_STATE_ERROR) { 66 mCallback.onError(); 67 } 68 } 69 } 70 71 @Override 72 public void onSessionStatusChanged(Bundle data, 73 String sessionId, MediaSessionStatus sessionStatus) { 74 logStatus("onSessionStatusChanged", sessionId, sessionStatus, null, null); 75 if (mCallback != null) { 76 mCallback.onPlaylistChanged(); 77 } 78 } 79 80 @Override 81 public void onSessionChanged(String sessionId) { 82 if (DEBUG) { 83 Log.d(TAG, "onSessionChanged: sessionId=" + sessionId); 84 } 85 } 86 }; 87 88 public RemotePlayer(Context context) { 89 mContext = context; 90 } 91 92 @Override 93 public boolean isRemotePlayback() { 94 return true; 95 } 96 97 @Override 98 public boolean isQueuingSupported() { 99 return mClient.isQueuingSupported(); 100 } 101 102 @Override 103 public void connect(RouteInfo route) { 104 mRoute = route; 105 mClient = new RemotePlaybackClient(mContext, route); 106 mClient.setStatusCallback(mStatusCallback); 107 108 if (DEBUG) { 109 Log.d(TAG, "connected to: " + route 110 + ", isRemotePlaybackSupported: " + mClient.isRemotePlaybackSupported() 111 + ", isQueuingSupported: "+ mClient.isQueuingSupported()); 112 } 113 } 114 115 @Override 116 public void release() { 117 mClient.release(); 118 119 if (DEBUG) { 120 Log.d(TAG, "released."); 121 } 122 } 123 124 // basic playback operations that are always supported 125 @Override 126 public void play(final PlaylistItem item) { 127 if (DEBUG) { 128 Log.d(TAG, "play: item=" + item); 129 } 130 mClient.play(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() { 131 @Override 132 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 133 String itemId, MediaItemStatus itemStatus) { 134 logStatus("play: succeeded", sessionId, sessionStatus, itemId, itemStatus); 135 item.setRemoteItemId(itemId); 136 if (item.getPosition() > 0) { 137 seekInternal(item); 138 } 139 if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { 140 pause(); 141 } else { 142 publishState(STATE_PLAYING); 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 publishState(STATE_PAUSED); 218 } 219 220 @Override 221 public void onError(String error, int code, Bundle data) { 222 logError("pause: failed", error, code); 223 } 224 }); 225 } 226 227 @Override 228 public void resume() { 229 if (!mClient.hasSession()) { 230 // ignore if no session 231 return; 232 } 233 if (DEBUG) { 234 Log.d(TAG, "resume"); 235 } 236 mClient.resume(null, new SessionActionCallback() { 237 @Override 238 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 239 logStatus("resume: succeeded", sessionId, sessionStatus, null, null); 240 if (mCallback != null) { 241 mCallback.onPlaylistChanged(); 242 } 243 publishState(STATE_PLAYING); 244 } 245 246 @Override 247 public void onError(String error, int code, Bundle data) { 248 logError("resume: failed", error, code); 249 } 250 }); 251 } 252 253 @Override 254 public void stop() { 255 if (!mClient.hasSession()) { 256 // ignore if no session 257 return; 258 } 259 publishState(STATE_IDLE); 260 if (DEBUG) { 261 Log.d(TAG, "stop"); 262 } 263 mClient.stop(null, new SessionActionCallback() { 264 @Override 265 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 266 logStatus("stop: succeeded", sessionId, sessionStatus, null, null); 267 if (mClient.isSessionManagementSupported()) { 268 endSession(); 269 } 270 if (mCallback != null) { 271 mCallback.onPlaylistChanged(); 272 } 273 } 274 275 @Override 276 public void onError(String error, int code, Bundle data) { 277 logError("stop: failed", error, code); 278 } 279 }); 280 } 281 282 // enqueue & remove are only supported if isQueuingSupported() returns true 283 @Override 284 public void enqueue(final PlaylistItem item) { 285 throwIfQueuingUnsupported(); 286 287 if (!mClient.hasSession() && !mEnqueuePending) { 288 mEnqueuePending = true; 289 if (mClient.isSessionManagementSupported()) { 290 startSession(item); 291 } else { 292 enqueueInternal(item); 293 } 294 } else if (mEnqueuePending){ 295 mTempQueue.add(item); 296 } else { 297 enqueueInternal(item); 298 } 299 } 300 301 @Override 302 public PlaylistItem remove(String itemId) { 303 throwIfNoSession(); 304 throwIfQueuingUnsupported(); 305 306 if (DEBUG) { 307 Log.d(TAG, "remove: itemId=" + itemId); 308 } 309 mClient.remove(itemId, null, new ItemActionCallback() { 310 @Override 311 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 312 String itemId, MediaItemStatus itemStatus) { 313 logStatus("remove: succeeded", sessionId, sessionStatus, itemId, itemStatus); 314 } 315 316 @Override 317 public void onError(String error, int code, Bundle data) { 318 logError("remove: failed", error, code); 319 } 320 }); 321 322 return null; 323 } 324 325 @Override 326 public void updateTrackInfo() { 327 // clear stats info first 328 mTrackInfo = ""; 329 mSnapshot = null; 330 331 Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_TRACK_INFO); 332 intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE); 333 334 if (mRoute != null && mRoute.supportsControlRequest(intent)) { 335 ControlRequestCallback callback = new ControlRequestCallback() { 336 @Override 337 public void onResult(Bundle data) { 338 if (DEBUG) { 339 Log.d(TAG, "getStatistics: succeeded: data=" + data); 340 } 341 if (data != null) { 342 mTrackInfo = data.getString(SampleMediaRouteProvider.TRACK_INFO_DESC); 343 mSnapshot = data.getParcelable( 344 SampleMediaRouteProvider.TRACK_INFO_SNAPSHOT); 345 } 346 } 347 348 @Override 349 public void onError(String error, Bundle data) { 350 Log.d(TAG, "getStatistics: failed: error=" + error + ", data=" + data); 351 } 352 }; 353 354 mRoute.sendControlRequest(intent, callback); 355 } 356 } 357 358 @Override 359 public String getDescription() { 360 return mTrackInfo; 361 } 362 363 @Override 364 public Bitmap getSnapshot() { 365 return mSnapshot; 366 } 367 368 private void enqueueInternal(final PlaylistItem item) { 369 throwIfQueuingUnsupported(); 370 371 if (DEBUG) { 372 Log.d(TAG, "enqueue: item=" + item); 373 } 374 mClient.enqueue(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() { 375 @Override 376 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 377 String itemId, MediaItemStatus itemStatus) { 378 logStatus("enqueue: succeeded", sessionId, sessionStatus, itemId, itemStatus); 379 item.setRemoteItemId(itemId); 380 if (item.getPosition() > 0) { 381 seekInternal(item); 382 } 383 if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { 384 pause(); 385 } else if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) { 386 publishState(STATE_PLAYING); 387 } 388 if (mEnqueuePending) { 389 mEnqueuePending = false; 390 for (PlaylistItem item : mTempQueue) { 391 enqueueInternal(item); 392 } 393 mTempQueue.clear(); 394 } 395 if (mCallback != null) { 396 mCallback.onPlaylistChanged(); 397 } 398 } 399 400 @Override 401 public void onError(String error, int code, Bundle data) { 402 logError("enqueue: failed", error, code); 403 if (mCallback != null) { 404 mCallback.onPlaylistChanged(); 405 } 406 } 407 }); 408 } 409 410 private void seekInternal(final PlaylistItem item) { 411 throwIfNoSession(); 412 413 if (DEBUG) { 414 Log.d(TAG, "seek: item=" + item); 415 } 416 mClient.seek(item.getRemoteItemId(), item.getPosition(), null, new ItemActionCallback() { 417 @Override 418 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 419 String itemId, MediaItemStatus itemStatus) { 420 logStatus("seek: succeeded", sessionId, sessionStatus, itemId, itemStatus); 421 if (mCallback != null) { 422 mCallback.onPlaylistChanged(); 423 } 424 } 425 426 @Override 427 public void onError(String error, int code, Bundle data) { 428 logError("seek: failed", error, code); 429 } 430 }); 431 } 432 433 private void startSession(final PlaylistItem item) { 434 mClient.startSession(null, new SessionActionCallback() { 435 @Override 436 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 437 logStatus("startSession: succeeded", sessionId, sessionStatus, null, null); 438 enqueueInternal(item); 439 } 440 441 @Override 442 public void onError(String error, int code, Bundle data) { 443 logError("startSession: failed", error, code); 444 } 445 }); 446 } 447 448 private void endSession() { 449 mClient.endSession(null, new SessionActionCallback() { 450 @Override 451 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 452 logStatus("endSession: succeeded", sessionId, sessionStatus, null, null); 453 } 454 455 @Override 456 public void onError(String error, int code, Bundle data) { 457 logError("endSession: failed", error, code); 458 } 459 }); 460 } 461 462 private void logStatus(String message, 463 String sessionId, MediaSessionStatus sessionStatus, 464 String itemId, MediaItemStatus itemStatus) { 465 if (DEBUG) { 466 String result = ""; 467 if (sessionId != null && sessionStatus != null) { 468 result += "sessionId=" + sessionId + ", sessionStatus=" + sessionStatus; 469 } 470 if (itemId != null & itemStatus != null) { 471 result += (result.isEmpty() ? "" : ", ") 472 + "itemId=" + itemId + ", itemStatus=" + itemStatus; 473 } 474 Log.d(TAG, message + ": " + result); 475 } 476 } 477 478 private void logError(String message, String error, int code) { 479 Log.d(TAG, message + ": error=" + error + ", code=" + code); 480 } 481 482 private void throwIfNoSession() { 483 if (!mClient.hasSession()) { 484 throw new IllegalStateException("Session is invalid"); 485 } 486 } 487 488 private void throwIfQueuingUnsupported() { 489 if (!isQueuingSupported()) { 490 throw new UnsupportedOperationException("Queuing is unsupported"); 491 } 492 } 493 } 494