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 
RemotePlayer(Context context)90     public RemotePlayer(Context context) {
91         mContext = context;
92     }
93 
94     @Override
isRemotePlayback()95     public boolean isRemotePlayback() {
96         return true;
97     }
98 
99     @Override
isQueuingSupported()100     public boolean isQueuingSupported() {
101         return mClient.isQueuingSupported();
102     }
103 
104     @Override
connect(RouteInfo route)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
release()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
play(final PlaylistItem item)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
seek(final PlaylistItem item)157     public void seek(final PlaylistItem item) {
158         seekInternal(item);
159     }
160 
161     @Override
getStatus(final PlaylistItem item, final boolean update)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
pause()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
resume()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
stop()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
enqueue(final PlaylistItem item)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
remove(String itemId)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
updateStatistics()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
getStatistics()355     public String getStatistics() {
356         return mStatsInfo;
357     }
358 
enqueueInternal(final PlaylistItem item)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 
seekInternal(final PlaylistItem item)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 
startSession(final PlaylistItem item)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 
endSession()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 
logStatus(String message, String sessionId, MediaSessionStatus sessionStatus, String itemId, MediaItemStatus itemStatus)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 
logError(String message, String error, int code)467     private void logError(String message, String error, int code) {
468         Log.d(TAG, message + ": error=" + error + ", code=" + code);
469     }
470 
throwIfNoSession()471     private void throwIfNoSession() {
472         if (!mClient.hasSession()) {
473             throw new IllegalStateException("Session is invalid");
474         }
475     }
476 
throwIfQueuingUnsupported()477     private void throwIfQueuingUnsupported() {
478         if (!isQueuingSupported()) {
479             throw new UnsupportedOperationException("Queuing is unsupported");
480         }
481     }
482 }
483