Home | History | Annotate | Download | only in media
      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 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