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