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.os.Bundle; 23import android.support.v7.media.MediaItemStatus; 24import android.support.v7.media.MediaRouter.ControlRequestCallback; 25import android.support.v7.media.MediaRouter.RouteInfo; 26import android.support.v7.media.MediaSessionStatus; 27import android.support.v7.media.RemotePlaybackClient; 28import android.support.v7.media.RemotePlaybackClient.ItemActionCallback; 29import android.support.v7.media.RemotePlaybackClient.SessionActionCallback; 30import android.support.v7.media.RemotePlaybackClient.StatusCallback; 31import android.util.Log; 32 33import java.util.ArrayList; 34import 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 */ 43public 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 Bitmap mSnapshot; 50 private List<PlaylistItem> mTempQueue = new ArrayList<PlaylistItem>(); 51 52 private RemotePlaybackClient mClient; 53 private StatusCallback mStatusCallback = new StatusCallback() { 54 @Override 55 public void onItemStatusChanged(Bundle data, 56 String sessionId, MediaSessionStatus sessionStatus, 57 String itemId, MediaItemStatus itemStatus) { 58 logStatus("onItemStatusChanged", sessionId, sessionStatus, itemId, itemStatus); 59 if (mCallback != null) { 60 mCallback.onPlaylistChanged(); 61 int state = itemStatus.getPlaybackState(); 62 if (state == MediaItemStatus.PLAYBACK_STATE_FINISHED) { 63 mCallback.onCompletion(); 64 } else if (state == MediaItemStatus.PLAYBACK_STATE_ERROR) { 65 mCallback.onError(); 66 } 67 } 68 } 69 70 @Override 71 public void onSessionStatusChanged(Bundle data, 72 String sessionId, MediaSessionStatus sessionStatus) { 73 logStatus("onSessionStatusChanged", sessionId, sessionStatus, null, null); 74 if (mCallback != null) { 75 mCallback.onPlaylistChanged(); 76 } 77 } 78 79 @Override 80 public void onSessionChanged(String sessionId) { 81 if (DEBUG) { 82 Log.d(TAG, "onSessionChanged: sessionId=" + sessionId); 83 } 84 } 85 }; 86 87 public RemotePlayer(Context context) { 88 mContext = context; 89 } 90 91 @Override 92 public boolean isRemotePlayback() { 93 return true; 94 } 95 96 @Override 97 public boolean isQueuingSupported() { 98 return mClient.isQueuingSupported(); 99 } 100 101 @Override 102 public void connect(RouteInfo route) { 103 mRoute = route; 104 mClient = new RemotePlaybackClient(mContext, route); 105 mClient.setStatusCallback(mStatusCallback); 106 107 if (DEBUG) { 108 Log.d(TAG, "connected to: " + route 109 + ", isRemotePlaybackSupported: " + mClient.isRemotePlaybackSupported() 110 + ", isQueuingSupported: "+ mClient.isQueuingSupported()); 111 } 112 } 113 114 @Override 115 public void release() { 116 mClient.release(); 117 118 if (DEBUG) { 119 Log.d(TAG, "released."); 120 } 121 } 122 123 // basic playback operations that are always supported 124 @Override 125 public void play(final PlaylistItem item) { 126 if (DEBUG) { 127 Log.d(TAG, "play: item=" + item); 128 } 129 mClient.play(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() { 130 @Override 131 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 132 String itemId, MediaItemStatus itemStatus) { 133 logStatus("play: succeeded", sessionId, sessionStatus, itemId, itemStatus); 134 item.setRemoteItemId(itemId); 135 if (item.getPosition() > 0) { 136 seekInternal(item); 137 } 138 if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { 139 pause(); 140 } else { 141 publishState(STATE_PLAYING); 142 } 143 if (mCallback != null) { 144 mCallback.onPlaylistChanged(); 145 } 146 } 147 148 @Override 149 public void onError(String error, int code, Bundle data) { 150 logError("play: failed", error, code); 151 } 152 }); 153 } 154 155 @Override 156 public void seek(final PlaylistItem item) { 157 seekInternal(item); 158 } 159 160 @Override 161 public void getStatus(final PlaylistItem item, final boolean update) { 162 if (!mClient.hasSession() || item.getRemoteItemId() == null) { 163 // if session is not valid or item id not assigend yet. 164 // just return, it's not fatal 165 return; 166 } 167 168 if (DEBUG) { 169 Log.d(TAG, "getStatus: item=" + item + ", update=" + update); 170 } 171 mClient.getStatus(item.getRemoteItemId(), null, new ItemActionCallback() { 172 @Override 173 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 174 String itemId, MediaItemStatus itemStatus) { 175 logStatus("getStatus: succeeded", sessionId, sessionStatus, itemId, itemStatus); 176 int state = itemStatus.getPlaybackState(); 177 if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING 178 || state == MediaItemStatus.PLAYBACK_STATE_PAUSED 179 || state == MediaItemStatus.PLAYBACK_STATE_PENDING) { 180 item.setState(state); 181 item.setPosition(itemStatus.getContentPosition()); 182 item.setDuration(itemStatus.getContentDuration()); 183 item.setTimestamp(itemStatus.getTimestamp()); 184 } 185 if (update && mCallback != null) { 186 mCallback.onPlaylistReady(); 187 } 188 } 189 190 @Override 191 public void onError(String error, int code, Bundle data) { 192 logError("getStatus: failed", error, code); 193 if (update && mCallback != null) { 194 mCallback.onPlaylistReady(); 195 } 196 } 197 }); 198 } 199 200 @Override 201 public void pause() { 202 if (!mClient.hasSession()) { 203 // ignore if no session 204 return; 205 } 206 if (DEBUG) { 207 Log.d(TAG, "pause"); 208 } 209 mClient.pause(null, new SessionActionCallback() { 210 @Override 211 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 212 logStatus("pause: succeeded", sessionId, sessionStatus, null, null); 213 if (mCallback != null) { 214 mCallback.onPlaylistChanged(); 215 } 216 publishState(STATE_PAUSED); 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 publishState(STATE_PLAYING); 243 } 244 245 @Override 246 public void onError(String error, int code, Bundle data) { 247 logError("resume: failed", error, code); 248 } 249 }); 250 } 251 252 @Override 253 public void stop() { 254 if (!mClient.hasSession()) { 255 // ignore if no session 256 return; 257 } 258 publishState(STATE_IDLE); 259 if (DEBUG) { 260 Log.d(TAG, "stop"); 261 } 262 mClient.stop(null, new SessionActionCallback() { 263 @Override 264 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 265 logStatus("stop: succeeded", sessionId, sessionStatus, null, null); 266 if (mClient.isSessionManagementSupported()) { 267 endSession(); 268 } 269 if (mCallback != null) { 270 mCallback.onPlaylistChanged(); 271 } 272 } 273 274 @Override 275 public void onError(String error, int code, Bundle data) { 276 logError("stop: failed", error, code); 277 } 278 }); 279 } 280 281 // enqueue & remove are only supported if isQueuingSupported() returns true 282 @Override 283 public void enqueue(final PlaylistItem item) { 284 throwIfQueuingUnsupported(); 285 286 if (!mClient.hasSession() && !mEnqueuePending) { 287 mEnqueuePending = true; 288 if (mClient.isSessionManagementSupported()) { 289 startSession(item); 290 } else { 291 enqueueInternal(item); 292 } 293 } else if (mEnqueuePending){ 294 mTempQueue.add(item); 295 } else { 296 enqueueInternal(item); 297 } 298 } 299 300 @Override 301 public PlaylistItem remove(String itemId) { 302 throwIfNoSession(); 303 throwIfQueuingUnsupported(); 304 305 if (DEBUG) { 306 Log.d(TAG, "remove: itemId=" + itemId); 307 } 308 mClient.remove(itemId, null, new ItemActionCallback() { 309 @Override 310 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 311 String itemId, MediaItemStatus itemStatus) { 312 logStatus("remove: succeeded", sessionId, sessionStatus, itemId, itemStatus); 313 } 314 315 @Override 316 public void onError(String error, int code, Bundle data) { 317 logError("remove: failed", error, code); 318 } 319 }); 320 321 return null; 322 } 323 324 @Override 325 public void takeSnapshot() { 326 mSnapshot = null; 327 328 Intent intent = new Intent(SampleMediaRouteProvider.ACTION_TAKE_SNAPSHOT); 329 intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE); 330 331 if (mRoute != null && mRoute.supportsControlRequest(intent)) { 332 ControlRequestCallback callback = new ControlRequestCallback() { 333 @Override 334 public void onResult(Bundle data) { 335 if (DEBUG) { 336 Log.d(TAG, "takeSnapshot: succeeded: data=" + data); 337 } 338 if (data != null) { 339 mSnapshot = data.getParcelable(SampleMediaRouteProvider.EXTRA_SNAPSHOT); 340 } 341 } 342 343 @Override 344 public void onError(String error, Bundle data) { 345 Log.d(TAG, "takeSnapshot: failed: error=" + error + ", data=" + data); 346 } 347 }; 348 349 mRoute.sendControlRequest(intent, callback); 350 } 351 } 352 353 @Override 354 public Bitmap getSnapshot() { 355 return mSnapshot; 356 } 357 358 private void enqueueInternal(final PlaylistItem item) { 359 throwIfQueuingUnsupported(); 360 361 if (DEBUG) { 362 Log.d(TAG, "enqueue: item=" + item); 363 } 364 mClient.enqueue(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() { 365 @Override 366 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 367 String itemId, MediaItemStatus itemStatus) { 368 logStatus("enqueue: succeeded", sessionId, sessionStatus, itemId, itemStatus); 369 item.setRemoteItemId(itemId); 370 if (item.getPosition() > 0) { 371 seekInternal(item); 372 } 373 if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { 374 pause(); 375 } else if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) { 376 publishState(STATE_PLAYING); 377 } 378 if (mEnqueuePending) { 379 mEnqueuePending = false; 380 for (PlaylistItem item : mTempQueue) { 381 enqueueInternal(item); 382 } 383 mTempQueue.clear(); 384 } 385 if (mCallback != null) { 386 mCallback.onPlaylistChanged(); 387 } 388 } 389 390 @Override 391 public void onError(String error, int code, Bundle data) { 392 logError("enqueue: failed", error, code); 393 if (mCallback != null) { 394 mCallback.onPlaylistChanged(); 395 } 396 } 397 }); 398 } 399 400 private void seekInternal(final PlaylistItem item) { 401 throwIfNoSession(); 402 403 if (DEBUG) { 404 Log.d(TAG, "seek: item=" + item); 405 } 406 mClient.seek(item.getRemoteItemId(), item.getPosition(), null, new ItemActionCallback() { 407 @Override 408 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, 409 String itemId, MediaItemStatus itemStatus) { 410 logStatus("seek: succeeded", sessionId, sessionStatus, itemId, itemStatus); 411 if (mCallback != null) { 412 mCallback.onPlaylistChanged(); 413 } 414 } 415 416 @Override 417 public void onError(String error, int code, Bundle data) { 418 logError("seek: failed", error, code); 419 } 420 }); 421 } 422 423 private void startSession(final PlaylistItem item) { 424 mClient.startSession(null, new SessionActionCallback() { 425 @Override 426 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 427 logStatus("startSession: succeeded", sessionId, sessionStatus, null, null); 428 enqueueInternal(item); 429 } 430 431 @Override 432 public void onError(String error, int code, Bundle data) { 433 logError("startSession: failed", error, code); 434 } 435 }); 436 } 437 438 private void endSession() { 439 mClient.endSession(null, new SessionActionCallback() { 440 @Override 441 public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { 442 logStatus("endSession: succeeded", sessionId, sessionStatus, null, null); 443 } 444 445 @Override 446 public void onError(String error, int code, Bundle data) { 447 logError("endSession: failed", error, code); 448 } 449 }); 450 } 451 452 private void logStatus(String message, 453 String sessionId, MediaSessionStatus sessionStatus, 454 String itemId, MediaItemStatus itemStatus) { 455 if (DEBUG) { 456 String result = ""; 457 if (sessionId != null && sessionStatus != null) { 458 result += "sessionId=" + sessionId + ", sessionStatus=" + sessionStatus; 459 } 460 if (itemId != null & itemStatus != null) { 461 result += (result.isEmpty() ? "" : ", ") 462 + "itemId=" + itemId + ", itemStatus=" + itemStatus; 463 } 464 Log.d(TAG, message + ": " + result); 465 } 466 } 467 468 private void logError(String message, String error, int code) { 469 Log.d(TAG, message + ": error=" + error + ", code=" + code); 470 } 471 472 private void throwIfNoSession() { 473 if (!mClient.hasSession()) { 474 throw new IllegalStateException("Session is invalid"); 475 } 476 } 477 478 private void throwIfQueuingUnsupported() { 479 if (!isQueuingSupported()) { 480 throw new UnsupportedOperationException("Queuing is unsupported"); 481 } 482 } 483} 484