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 String mTrackInfo = "";
     50     private Bitmap mSnapshot;
     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                 } else {
    142                     publishState(STATE_PLAYING);
    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                 publishState(STATE_PAUSED);
    218             }
    219 
    220             @Override
    221             public void onError(String error, int code, Bundle data) {
    222                 logError("pause: failed", error, code);
    223             }
    224         });
    225     }
    226 
    227     @Override
    228     public void resume() {
    229         if (!mClient.hasSession()) {
    230             // ignore if no session
    231             return;
    232         }
    233         if (DEBUG) {
    234             Log.d(TAG, "resume");
    235         }
    236         mClient.resume(null, new SessionActionCallback() {
    237             @Override
    238             public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
    239                 logStatus("resume: succeeded", sessionId, sessionStatus, null, null);
    240                 if (mCallback != null) {
    241                     mCallback.onPlaylistChanged();
    242                 }
    243                 publishState(STATE_PLAYING);
    244             }
    245 
    246             @Override
    247             public void onError(String error, int code, Bundle data) {
    248                 logError("resume: failed", error, code);
    249             }
    250         });
    251     }
    252 
    253     @Override
    254     public void stop() {
    255         if (!mClient.hasSession()) {
    256             // ignore if no session
    257             return;
    258         }
    259         publishState(STATE_IDLE);
    260         if (DEBUG) {
    261             Log.d(TAG, "stop");
    262         }
    263         mClient.stop(null, new SessionActionCallback() {
    264             @Override
    265             public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
    266                 logStatus("stop: succeeded", sessionId, sessionStatus, null, null);
    267                 if (mClient.isSessionManagementSupported()) {
    268                     endSession();
    269                 }
    270                 if (mCallback != null) {
    271                     mCallback.onPlaylistChanged();
    272                 }
    273             }
    274 
    275             @Override
    276             public void onError(String error, int code, Bundle data) {
    277                 logError("stop: failed", error, code);
    278             }
    279         });
    280     }
    281 
    282     // enqueue & remove are only supported if isQueuingSupported() returns true
    283     @Override
    284     public void enqueue(final PlaylistItem item) {
    285         throwIfQueuingUnsupported();
    286 
    287         if (!mClient.hasSession() && !mEnqueuePending) {
    288             mEnqueuePending = true;
    289             if (mClient.isSessionManagementSupported()) {
    290                 startSession(item);
    291             } else {
    292                 enqueueInternal(item);
    293             }
    294         } else if (mEnqueuePending){
    295             mTempQueue.add(item);
    296         } else {
    297             enqueueInternal(item);
    298         }
    299     }
    300 
    301     @Override
    302     public PlaylistItem remove(String itemId) {
    303         throwIfNoSession();
    304         throwIfQueuingUnsupported();
    305 
    306         if (DEBUG) {
    307             Log.d(TAG, "remove: itemId=" + itemId);
    308         }
    309         mClient.remove(itemId, null, new ItemActionCallback() {
    310             @Override
    311             public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
    312                     String itemId, MediaItemStatus itemStatus) {
    313                 logStatus("remove: succeeded", sessionId, sessionStatus, itemId, itemStatus);
    314             }
    315 
    316             @Override
    317             public void onError(String error, int code, Bundle data) {
    318                 logError("remove: failed", error, code);
    319             }
    320         });
    321 
    322         return null;
    323     }
    324 
    325     @Override
    326     public void updateTrackInfo() {
    327         // clear stats info first
    328         mTrackInfo = "";
    329         mSnapshot = null;
    330 
    331         Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_TRACK_INFO);
    332         intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE);
    333 
    334         if (mRoute != null && mRoute.supportsControlRequest(intent)) {
    335             ControlRequestCallback callback = new ControlRequestCallback() {
    336                 @Override
    337                 public void onResult(Bundle data) {
    338                     if (DEBUG) {
    339                         Log.d(TAG, "getStatistics: succeeded: data=" + data);
    340                     }
    341                     if (data != null) {
    342                         mTrackInfo = data.getString(SampleMediaRouteProvider.TRACK_INFO_DESC);
    343                         mSnapshot = data.getParcelable(
    344                                 SampleMediaRouteProvider.TRACK_INFO_SNAPSHOT);
    345                     }
    346                 }
    347 
    348                 @Override
    349                 public void onError(String error, Bundle data) {
    350                     Log.d(TAG, "getStatistics: failed: error=" + error + ", data=" + data);
    351                 }
    352             };
    353 
    354             mRoute.sendControlRequest(intent, callback);
    355         }
    356     }
    357 
    358     @Override
    359     public String getDescription() {
    360         return mTrackInfo;
    361     }
    362 
    363     @Override
    364     public Bitmap getSnapshot() {
    365         return mSnapshot;
    366     }
    367 
    368     private void enqueueInternal(final PlaylistItem item) {
    369         throwIfQueuingUnsupported();
    370 
    371         if (DEBUG) {
    372             Log.d(TAG, "enqueue: item=" + item);
    373         }
    374         mClient.enqueue(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() {
    375             @Override
    376             public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
    377                     String itemId, MediaItemStatus itemStatus) {
    378                 logStatus("enqueue: succeeded", sessionId, sessionStatus, itemId, itemStatus);
    379                 item.setRemoteItemId(itemId);
    380                 if (item.getPosition() > 0) {
    381                     seekInternal(item);
    382                 }
    383                 if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
    384                     pause();
    385                 } else if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
    386                     publishState(STATE_PLAYING);
    387                 }
    388                 if (mEnqueuePending) {
    389                     mEnqueuePending = false;
    390                     for (PlaylistItem item : mTempQueue) {
    391                         enqueueInternal(item);
    392                     }
    393                     mTempQueue.clear();
    394                 }
    395                 if (mCallback != null) {
    396                     mCallback.onPlaylistChanged();
    397                 }
    398             }
    399 
    400             @Override
    401             public void onError(String error, int code, Bundle data) {
    402                 logError("enqueue: failed", error, code);
    403                 if (mCallback != null) {
    404                     mCallback.onPlaylistChanged();
    405                 }
    406             }
    407         });
    408     }
    409 
    410     private void seekInternal(final PlaylistItem item) {
    411         throwIfNoSession();
    412 
    413         if (DEBUG) {
    414             Log.d(TAG, "seek: item=" + item);
    415         }
    416         mClient.seek(item.getRemoteItemId(), item.getPosition(), null, new ItemActionCallback() {
    417            @Override
    418            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
    419                    String itemId, MediaItemStatus itemStatus) {
    420                logStatus("seek: succeeded", sessionId, sessionStatus, itemId, itemStatus);
    421                if (mCallback != null) {
    422                    mCallback.onPlaylistChanged();
    423                }
    424            }
    425 
    426            @Override
    427            public void onError(String error, int code, Bundle data) {
    428                logError("seek: failed", error, code);
    429            }
    430         });
    431     }
    432 
    433     private void startSession(final PlaylistItem item) {
    434         mClient.startSession(null, new SessionActionCallback() {
    435             @Override
    436             public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
    437                 logStatus("startSession: succeeded", sessionId, sessionStatus, null, null);
    438                 enqueueInternal(item);
    439             }
    440 
    441             @Override
    442             public void onError(String error, int code, Bundle data) {
    443                 logError("startSession: failed", error, code);
    444             }
    445         });
    446     }
    447 
    448     private void endSession() {
    449         mClient.endSession(null, new SessionActionCallback() {
    450             @Override
    451             public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
    452                 logStatus("endSession: succeeded", sessionId, sessionStatus, null, null);
    453             }
    454 
    455             @Override
    456             public void onError(String error, int code, Bundle data) {
    457                 logError("endSession: failed", error, code);
    458             }
    459         });
    460     }
    461 
    462     private void logStatus(String message,
    463             String sessionId, MediaSessionStatus sessionStatus,
    464             String itemId, MediaItemStatus itemStatus) {
    465         if (DEBUG) {
    466             String result = "";
    467             if (sessionId != null && sessionStatus != null) {
    468                 result += "sessionId=" + sessionId + ", sessionStatus=" + sessionStatus;
    469             }
    470             if (itemId != null & itemStatus != null) {
    471                 result += (result.isEmpty() ? "" : ", ")
    472                         + "itemId=" + itemId + ", itemStatus=" + itemStatus;
    473             }
    474             Log.d(TAG, message + ": " + result);
    475         }
    476     }
    477 
    478     private void logError(String message, String error, int code) {
    479         Log.d(TAG, message + ": error=" + error + ", code=" + code);
    480     }
    481 
    482     private void throwIfNoSession() {
    483         if (!mClient.hasSession()) {
    484             throw new IllegalStateException("Session is invalid");
    485         }
    486     }
    487 
    488     private void throwIfQueuingUnsupported() {
    489         if (!isQueuingSupported()) {
    490             throw new UnsupportedOperationException("Queuing is unsupported");
    491         }
    492     }
    493 }
    494