Home | History | Annotate | Download | only in player
      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.mediarouter.player;
     18 
     19 import android.content.Context;
     20 import android.content.Intent;
     21 import android.os.Bundle;
     22 import android.support.v7.media.MediaItemStatus;
     23 import android.support.v7.media.MediaRouter.ControlRequestCallback;
     24 import android.support.v7.media.MediaRouter.RouteInfo;
     25 import android.support.v7.media.MediaSessionStatus;
     26 import android.support.v7.media.RemotePlaybackClient;
     27 import android.support.v7.media.RemotePlaybackClient.ItemActionCallback;
     28 import android.support.v7.media.RemotePlaybackClient.SessionActionCallback;
     29 import android.support.v7.media.RemotePlaybackClient.StatusCallback;
     30 import android.util.Log;
     31 
     32 import com.example.android.mediarouter.player.Player;
     33 import com.example.android.mediarouter.player.PlaylistItem;
     34 import com.example.android.mediarouter.provider.SampleMediaRouteProvider;
     35 
     36 import java.util.ArrayList;
     37 import java.util.List;
     38 
     39 /**
     40  * Handles playback of media items using a remote route.
     41  *
     42  * This class is used as a backend by PlaybackManager to feed media items to
     43  * the remote route. When the remote route doesn't support queuing, media items
     44  * are fed one-at-a-time; otherwise media items are enqueued to the remote side.
     45  */
     46 public class RemotePlayer extends Player {
     47     private static final String TAG = "RemotePlayer";
     48     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     49     private Context mContext;
     50     private RouteInfo mRoute;
     51     private boolean mEnqueuePending;
     52     private String mStatsInfo = "";
     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                 }
    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             }
    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             }
    243 
    244             @Override
    245             public void onError(String error, int code, Bundle data) {
    246                 logError("resume: failed", error, code);
    247             }
    248         });
    249     }
    250 
    251     @Override
    252     public void stop() {
    253         if (!mClient.hasSession()) {
    254             // ignore if no session
    255             return;
    256         }
    257         if (DEBUG) {
    258             Log.d(TAG, "stop");
    259         }
    260         mClient.stop(null, new SessionActionCallback() {
    261             @Override
    262             public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
    263                 logStatus("stop: succeeded", sessionId, sessionStatus, null, null);
    264                 if (mClient.isSessionManagementSupported()) {
    265                     endSession();
    266                 }
    267                 if (mCallback != null) {
    268                     mCallback.onPlaylistChanged();
    269                 }
    270             }
    271 
    272             @Override
    273             public void onError(String error, int code, Bundle data) {
    274                 logError("stop: failed", error, code);
    275             }
    276         });
    277     }
    278 
    279     // enqueue & remove are only supported if isQueuingSupported() returns true
    280     @Override
    281     public void enqueue(final PlaylistItem item) {
    282         throwIfQueuingUnsupported();
    283 
    284         if (!mClient.hasSession() && !mEnqueuePending) {
    285             mEnqueuePending = true;
    286             if (mClient.isSessionManagementSupported()) {
    287                 startSession(item);
    288             } else {
    289                 enqueueInternal(item);
    290             }
    291         } else if (mEnqueuePending){
    292             mTempQueue.add(item);
    293         } else {
    294             enqueueInternal(item);
    295         }
    296     }
    297 
    298     @Override
    299     public PlaylistItem remove(String itemId) {
    300         throwIfNoSession();
    301         throwIfQueuingUnsupported();
    302 
    303         if (DEBUG) {
    304             Log.d(TAG, "remove: itemId=" + itemId);
    305         }
    306         mClient.remove(itemId, null, new ItemActionCallback() {
    307             @Override
    308             public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
    309                     String itemId, MediaItemStatus itemStatus) {
    310                 logStatus("remove: succeeded", sessionId, sessionStatus, itemId, itemStatus);
    311             }
    312 
    313             @Override
    314             public void onError(String error, int code, Bundle data) {
    315                 logError("remove: failed", error, code);
    316             }
    317         });
    318 
    319         return null;
    320     }
    321 
    322     @Override
    323     public void updateStatistics() {
    324         // clear stats info first
    325         mStatsInfo = "";
    326 
    327         Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_STATISTICS);
    328         intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE);
    329 
    330         if (mRoute != null && mRoute.supportsControlRequest(intent)) {
    331             ControlRequestCallback callback = new ControlRequestCallback() {
    332                 @Override
    333                 public void onResult(Bundle data) {
    334                     if (DEBUG) {
    335                         Log.d(TAG, "getStatistics: succeeded: data=" + data);
    336                     }
    337                     if (data != null) {
    338                         int playbackCount = data.getInt(
    339                                 SampleMediaRouteProvider.DATA_PLAYBACK_COUNT, -1);
    340                         mStatsInfo = "Total playback count: " + playbackCount;
    341                     }
    342                 }
    343 
    344                 @Override
    345                 public void onError(String error, Bundle data) {
    346                     Log.d(TAG, "getStatistics: failed: error=" + error + ", data=" + data);
    347                 }
    348             };
    349 
    350             mRoute.sendControlRequest(intent, callback);
    351         }
    352     }
    353 
    354     @Override
    355     public String getStatistics() {
    356         return mStatsInfo;
    357     }
    358 
    359     private void enqueueInternal(final PlaylistItem item) {
    360         throwIfQueuingUnsupported();
    361 
    362         if (DEBUG) {
    363             Log.d(TAG, "enqueue: item=" + item);
    364         }
    365         mClient.enqueue(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() {
    366             @Override
    367             public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
    368                     String itemId, MediaItemStatus itemStatus) {
    369                 logStatus("enqueue: succeeded", sessionId, sessionStatus, itemId, itemStatus);
    370                 item.setRemoteItemId(itemId);
    371                 if (item.getPosition() > 0) {
    372                     seekInternal(item);
    373                 }
    374                 if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
    375                     pause();
    376                 }
    377                 if (mEnqueuePending) {
    378                     mEnqueuePending = false;
    379                     for (PlaylistItem item : mTempQueue) {
    380                         enqueueInternal(item);
    381                     }
    382                     mTempQueue.clear();
    383                 }
    384                 if (mCallback != null) {
    385                     mCallback.onPlaylistChanged();
    386                 }
    387             }
    388 
    389             @Override
    390             public void onError(String error, int code, Bundle data) {
    391                 logError("enqueue: failed", error, code);
    392                 if (mCallback != null) {
    393                     mCallback.onPlaylistChanged();
    394                 }
    395             }
    396         });
    397     }
    398 
    399     private void seekInternal(final PlaylistItem item) {
    400         throwIfNoSession();
    401 
    402         if (DEBUG) {
    403             Log.d(TAG, "seek: item=" + item);
    404         }
    405         mClient.seek(item.getRemoteItemId(), item.getPosition(), null, new ItemActionCallback() {
    406            @Override
    407            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
    408                    String itemId, MediaItemStatus itemStatus) {
    409                logStatus("seek: succeeded", sessionId, sessionStatus, itemId, itemStatus);
    410                if (mCallback != null) {
    411                    mCallback.onPlaylistChanged();
    412                }
    413            }
    414 
    415            @Override
    416            public void onError(String error, int code, Bundle data) {
    417                logError("seek: failed", error, code);
    418            }
    419         });
    420     }
    421 
    422     private void startSession(final PlaylistItem item) {
    423         mClient.startSession(null, new SessionActionCallback() {
    424             @Override
    425             public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
    426                 logStatus("startSession: succeeded", sessionId, sessionStatus, null, null);
    427                 enqueueInternal(item);
    428             }
    429 
    430             @Override
    431             public void onError(String error, int code, Bundle data) {
    432                 logError("startSession: failed", error, code);
    433             }
    434         });
    435     }
    436 
    437     private void endSession() {
    438         mClient.endSession(null, new SessionActionCallback() {
    439             @Override
    440             public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
    441                 logStatus("endSession: succeeded", sessionId, sessionStatus, null, null);
    442             }
    443 
    444             @Override
    445             public void onError(String error, int code, Bundle data) {
    446                 logError("endSession: failed", error, code);
    447             }
    448         });
    449     }
    450 
    451     private void logStatus(String message,
    452             String sessionId, MediaSessionStatus sessionStatus,
    453             String itemId, MediaItemStatus itemStatus) {
    454         if (DEBUG) {
    455             String result = "";
    456             if (sessionId != null && sessionStatus != null) {
    457                 result += "sessionId=" + sessionId + ", sessionStatus=" + sessionStatus;
    458             }
    459             if (itemId != null & itemStatus != null) {
    460                 result += (result.isEmpty() ? "" : ", ")
    461                         + "itemId=" + itemId + ", itemStatus=" + itemStatus;
    462             }
    463             Log.d(TAG, message + ": " + result);
    464         }
    465     }
    466 
    467     private void logError(String message, String error, int code) {
    468         Log.d(TAG, message + ": error=" + error + ", code=" + code);
    469     }
    470 
    471     private void throwIfNoSession() {
    472         if (!mClient.hasSession()) {
    473             throw new IllegalStateException("Session is invalid");
    474         }
    475     }
    476 
    477     private void throwIfQueuingUnsupported() {
    478         if (!isQueuingSupported()) {
    479             throw new UnsupportedOperationException("Queuing is unsupported");
    480         }
    481     }
    482 }
    483