RemotePlayer.java revision 76d965dc41863b33f887db33d283cb7f1523f60d
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.graphics.Bitmap;
22import android.os.Bundle;
23import android.support.v7.media.MediaItemStatus;
24import android.support.v7.media.MediaRouter.ControlRequestCallback;
25import android.support.v7.media.MediaRouter.RouteInfo;
26import android.support.v7.media.MediaSessionStatus;
27import android.support.v7.media.RemotePlaybackClient;
28import android.support.v7.media.RemotePlaybackClient.ItemActionCallback;
29import android.support.v7.media.RemotePlaybackClient.SessionActionCallback;
30import android.support.v7.media.RemotePlaybackClient.StatusCallback;
31import android.util.Log;
32
33import java.util.ArrayList;
34import 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 */
43public 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