RemotePlayer.java revision 5d429bc3a8195d6f37cf2f7da0935972950539b4
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 17package com.example.android.supportv7.media; 18 19import android.content.Context; 20import android.content.Intent; 21import android.graphics.Bitmap; 22import android.net.Uri; 23import android.os.Bundle; 24import android.support.v7.media.MediaItemStatus; 25import android.support.v7.media.MediaControlIntent; 26import android.support.v7.media.MediaRouter.ControlRequestCallback; 27import android.support.v7.media.MediaRouter.RouteInfo; 28import android.support.v7.media.MediaSessionStatus; 29import android.support.v7.media.RemotePlaybackClient; 30import android.support.v7.media.RemotePlaybackClient.ItemActionCallback; 31import android.support.v7.media.RemotePlaybackClient.SessionActionCallback; 32import android.support.v7.media.RemotePlaybackClient.StatusCallback; 33import android.util.Log; 34 35import java.util.ArrayList; 36import java.util.List; 37 38/** 39 * Handles playback of media items using a remote route. 40 * 41 * This class is used as a backend by PlaybackManager to feed media items to 42 * the remote route. When the remote route doesn't support queuing, media items 43 * are fed one-at-a-time; otherwise media items are enqueued to the remote side. 44 */ 45public class RemotePlayer extends Player { 46 private static final String TAG = "RemotePlayer"; 47 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 48 private Context mContext; 49 private RouteInfo mRoute; 50 private boolean mEnqueuePending; 51 private String mTrackInfo = ""; 52 private Bitmap mSnapshot; 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 publishState(STATE_PAUSED); 144 } else { 145 publishState(STATE_PLAYING); 146 } 147 if (mCallback != null) { 148 mCallback.onPlaylistChanged(); 149 } 150 } 151 152 @Override 153 public void onError(String error, int code, Bundle data) { 154 logError("play: failed", error, code); 155 } 156 }); 157 } 158 159 @Override 160 public void seek(final PlaylistItem item) { 161 seekInternal(item); 162 } 163 164 @Override 165 public void getStatus(final PlaylistItem item, final boolean update) { 166 if (!mClient.hasSession() || item.getRemoteItemId() == null) { 167 // if session is not valid or item id not assigend yet. 168 // just return, it's not fatal 169 return; 170 } 171 172 if (DEBUG) { 173 Log.d(TAG, "getStatus: item=" + item + ", update=" + update); 174 } 175 mClient.getStatus(item.getRemoteItemId(), null, new ItemActionCallback() { 176 @Override 177 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 178 String itemId, MediaItemStatus itemStatus) { 179 logStatus("getStatus: succeeded", sessionId, sessionStatus, itemId, itemStatus); 180 int state = itemStatus.getPlaybackState(); 181 if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING 182 || state == MediaItemStatus.PLAYBACK_STATE_PAUSED 183 || state == MediaItemStatus.PLAYBACK_STATE_PENDING) { 184 item.setState(state); 185 item.setPosition(itemStatus.getContentPosition()); 186 item.setDuration(itemStatus.getContentDuration()); 187 item.setTimestamp(itemStatus.getTimestamp()); 188 } 189 if (update && mCallback != null) { 190 mCallback.onPlaylistReady(); 191 } 192 } 193 194 @Override 195 public void onError(String error, int code, Bundle data) { 196 logError("getStatus: failed", error, code); 197 if (update && mCallback != null) { 198 mCallback.onPlaylistReady(); 199 } 200 } 201 }); 202 } 203 204 @Override 205 public void pause() { 206 if (!mClient.hasSession()) { 207 // ignore if no session 208 return; 209 } 210 if (DEBUG) { 211 Log.d(TAG, "pause"); 212 } 213 mClient.pause(null, new SessionActionCallback() { 214 @Override 215 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 216 logStatus("pause: succeeded", sessionId, sessionStatus, null, null); 217 if (mCallback != null) { 218 mCallback.onPlaylistChanged(); 219 } 220 publishState(STATE_PAUSED); 221 } 222 223 @Override 224 public void onError(String error, int code, Bundle data) { 225 logError("pause: failed", error, code); 226 } 227 }); 228 } 229 230 @Override 231 public void resume() { 232 if (!mClient.hasSession()) { 233 // ignore if no session 234 return; 235 } 236 if (DEBUG) { 237 Log.d(TAG, "resume"); 238 } 239 mClient.resume(null, new SessionActionCallback() { 240 @Override 241 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 242 logStatus("resume: succeeded", sessionId, sessionStatus, null, null); 243 if (mCallback != null) { 244 mCallback.onPlaylistChanged(); 245 } 246 publishState(STATE_PLAYING); 247 } 248 249 @Override 250 public void onError(String error, int code, Bundle data) { 251 logError("resume: failed", error, code); 252 } 253 }); 254 } 255 256 @Override 257 public void stop() { 258 if (!mClient.hasSession()) { 259 // ignore if no session 260 return; 261 } 262 publishState(STATE_IDLE); 263 if (DEBUG) { 264 Log.d(TAG, "stop"); 265 } 266 mClient.stop(null, new SessionActionCallback() { 267 @Override 268 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 269 logStatus("stop: succeeded", sessionId, sessionStatus, null, null); 270 if (mClient.isSessionManagementSupported()) { 271 endSession(); 272 } 273 if (mCallback != null) { 274 mCallback.onPlaylistChanged(); 275 } 276 } 277 278 @Override 279 public void onError(String error, int code, Bundle data) { 280 logError("stop: failed", error, code); 281 } 282 }); 283 } 284 285 // enqueue & remove are only supported if isQueuingSupported() returns true 286 @Override 287 public void enqueue(final PlaylistItem item) { 288 throwIfQueuingUnsupported(); 289 290 if (!mClient.hasSession() && !mEnqueuePending) { 291 mEnqueuePending = true; 292 if (mClient.isSessionManagementSupported()) { 293 startSession(item); 294 } else { 295 enqueueInternal(item); 296 } 297 } else if (mEnqueuePending){ 298 mTempQueue.add(item); 299 } else { 300 enqueueInternal(item); 301 } 302 } 303 304 @Override 305 public PlaylistItem remove(String itemId) { 306 throwIfNoSession(); 307 throwIfQueuingUnsupported(); 308 309 if (DEBUG) { 310 Log.d(TAG, "remove: itemId=" + itemId); 311 } 312 mClient.remove(itemId, null, new ItemActionCallback() { 313 @Override 314 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 315 String itemId, MediaItemStatus itemStatus) { 316 logStatus("remove: succeeded", sessionId, sessionStatus, itemId, itemStatus); 317 } 318 319 @Override 320 public void onError(String error, int code, Bundle data) { 321 logError("remove: failed", error, code); 322 } 323 }); 324 325 return null; 326 } 327 328 @Override 329 public void updateTrackInfo() { 330 // clear stats info first 331 mTrackInfo = ""; 332 mSnapshot = null; 333 334 Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_TRACK_INFO); 335 intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE); 336 337 if (mRoute != null && mRoute.supportsControlRequest(intent)) { 338 ControlRequestCallback callback = new ControlRequestCallback() { 339 @Override 340 public void onResult(Bundle data) { 341 if (DEBUG) { 342 Log.d(TAG, "getStatistics: succeeded: data=" + data); 343 } 344 if (data != null) { 345 mTrackInfo = data.getString(SampleMediaRouteProvider.TRACK_INFO_DESC); 346 mSnapshot = data.getParcelable( 347 SampleMediaRouteProvider.TRACK_INFO_SNAPSHOT); 348 } 349 } 350 351 @Override 352 public void onError(String error, Bundle data) { 353 Log.d(TAG, "getStatistics: failed: error=" + error + ", data=" + data); 354 } 355 }; 356 357 mRoute.sendControlRequest(intent, callback); 358 } 359 } 360 361 @Override 362 public String getDescription() { 363 return mTrackInfo; 364 } 365 366 @Override 367 public Bitmap getSnapshot() { 368 return mSnapshot; 369 } 370 371 private void enqueueInternal(final PlaylistItem item) { 372 throwIfQueuingUnsupported(); 373 374 if (DEBUG) { 375 Log.d(TAG, "enqueue: item=" + item); 376 } 377 mClient.enqueue(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() { 378 @Override 379 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 380 String itemId, MediaItemStatus itemStatus) { 381 logStatus("enqueue: succeeded", sessionId, sessionStatus, itemId, itemStatus); 382 item.setRemoteItemId(itemId); 383 if (item.getPosition() > 0) { 384 seekInternal(item); 385 } 386 if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { 387 pause(); 388 } 389 if (mEnqueuePending) { 390 mEnqueuePending = false; 391 for (PlaylistItem item : mTempQueue) { 392 enqueueInternal(item); 393 } 394 mTempQueue.clear(); 395 } 396 if (mCallback != null) { 397 mCallback.onPlaylistChanged(); 398 } 399 } 400 401 @Override 402 public void onError(String error, int code, Bundle data) { 403 logError("enqueue: failed", error, code); 404 if (mCallback != null) { 405 mCallback.onPlaylistChanged(); 406 } 407 } 408 }); 409 } 410 411 private void seekInternal(final PlaylistItem item) { 412 throwIfNoSession(); 413 414 if (DEBUG) { 415 Log.d(TAG, "seek: item=" + item); 416 } 417 mClient.seek(item.getRemoteItemId(), item.getPosition(), null, new ItemActionCallback() { 418 @Override 419 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 420 String itemId, MediaItemStatus itemStatus) { 421 logStatus("seek: succeeded", sessionId, sessionStatus, itemId, itemStatus); 422 if (mCallback != null) { 423 mCallback.onPlaylistChanged(); 424 } 425 } 426 427 @Override 428 public void onError(String error, int code, Bundle data) { 429 logError("seek: failed", error, code); 430 } 431 }); 432 } 433 434 private void startSession(final PlaylistItem item) { 435 mClient.startSession(null, new SessionActionCallback() { 436 @Override 437 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 438 logStatus("startSession: succeeded", sessionId, sessionStatus, null, null); 439 enqueueInternal(item); 440 } 441 442 @Override 443 public void onError(String error, int code, Bundle data) { 444 logError("startSession: failed", error, code); 445 } 446 }); 447 } 448 449 private void endSession() { 450 mClient.endSession(null, new SessionActionCallback() { 451 @Override 452 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 453 logStatus("endSession: succeeded", sessionId, sessionStatus, null, null); 454 } 455 456 @Override 457 public void onError(String error, int code, Bundle data) { 458 logError("endSession: failed", error, code); 459 } 460 }); 461 } 462 463 private void logStatus(String message, 464 String sessionId, MediaSessionStatus sessionStatus, 465 String itemId, MediaItemStatus itemStatus) { 466 if (DEBUG) { 467 String result = ""; 468 if (sessionId != null && sessionStatus != null) { 469 result += "sessionId=" + sessionId + ", sessionStatus=" + sessionStatus; 470 } 471 if (itemId != null & itemStatus != null) { 472 result += (result.isEmpty() ? "" : ", ") 473 + "itemId=" + itemId + ", itemStatus=" + itemStatus; 474 } 475 Log.d(TAG, message + ": " + result); 476 } 477 } 478 479 private void logError(String message, String error, int code) { 480 Log.d(TAG, message + ": error=" + error + ", code=" + code); 481 } 482 483 private void throwIfNoSession() { 484 if (!mClient.hasSession()) { 485 throw new IllegalStateException("Session is invalid"); 486 } 487 } 488 489 private void throwIfQueuingUnsupported() { 490 if (!isQueuingSupported()) { 491 throw new UnsupportedOperationException("Queuing is unsupported"); 492 } 493 } 494} 495