RemotePlayer.java revision 5d429bc3a8195d6f37cf2f7da0935972950539b4
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.net.Uri;
23import android.os.Bundle;
24import android.support.v7.media.MediaItemStatus;
25import android.support.v7.media.MediaControlIntent;
26import android.support.v7.media.MediaRouter.ControlRequestCallback;
27import android.support.v7.media.MediaRouter.RouteInfo;
28import android.support.v7.media.MediaSessionStatus;
29import android.support.v7.media.RemotePlaybackClient;
30import android.support.v7.media.RemotePlaybackClient.ItemActionCallback;
31import android.support.v7.media.RemotePlaybackClient.SessionActionCallback;
32import android.support.v7.media.RemotePlaybackClient.StatusCallback;
33import android.util.Log;
34
35import java.util.ArrayList;
36import java.util.List;
37
38/**
39 * Handles playback of media items using a remote route.
40 *
41 * This class is used as a backend by PlaybackManager to feed media items to
42 * the remote route. When the remote route doesn't support queuing, media items
43 * are fed one-at-a-time; otherwise media items are enqueued to the remote side.
44 */
45public class RemotePlayer extends Player {
46    private static final String TAG = "RemotePlayer";
47    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
48    private Context mContext;
49    private RouteInfo mRoute;
50    private boolean mEnqueuePending;
51    private String mTrackInfo = "";
52    private Bitmap mSnapshot;
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                    publishState(STATE_PAUSED);
144                } else {
145                    publishState(STATE_PLAYING);
146                }
147                if (mCallback != null) {
148                    mCallback.onPlaylistChanged();
149                }
150            }
151
152            @Override
153            public void onError(String error, int code, Bundle data) {
154                logError("play: failed", error, code);
155            }
156        });
157    }
158
159    @Override
160    public void seek(final PlaylistItem item) {
161        seekInternal(item);
162    }
163
164    @Override
165    public void getStatus(final PlaylistItem item, final boolean update) {
166        if (!mClient.hasSession() || item.getRemoteItemId() == null) {
167            // if session is not valid or item id not assigend yet.
168            // just return, it's not fatal
169            return;
170        }
171
172        if (DEBUG) {
173            Log.d(TAG, "getStatus: item=" + item + ", update=" + update);
174        }
175        mClient.getStatus(item.getRemoteItemId(), null, new ItemActionCallback() {
176            @Override
177            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
178                    String itemId, MediaItemStatus itemStatus) {
179                logStatus("getStatus: succeeded", sessionId, sessionStatus, itemId, itemStatus);
180                int state = itemStatus.getPlaybackState();
181                if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING
182                        || state == MediaItemStatus.PLAYBACK_STATE_PAUSED
183                        || state == MediaItemStatus.PLAYBACK_STATE_PENDING) {
184                    item.setState(state);
185                    item.setPosition(itemStatus.getContentPosition());
186                    item.setDuration(itemStatus.getContentDuration());
187                    item.setTimestamp(itemStatus.getTimestamp());
188                }
189                if (update && mCallback != null) {
190                    mCallback.onPlaylistReady();
191                }
192            }
193
194            @Override
195            public void onError(String error, int code, Bundle data) {
196                logError("getStatus: failed", error, code);
197                if (update && mCallback != null) {
198                    mCallback.onPlaylistReady();
199                }
200            }
201        });
202    }
203
204    @Override
205    public void pause() {
206        if (!mClient.hasSession()) {
207            // ignore if no session
208            return;
209        }
210        if (DEBUG) {
211            Log.d(TAG, "pause");
212        }
213        mClient.pause(null, new SessionActionCallback() {
214            @Override
215            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
216                logStatus("pause: succeeded", sessionId, sessionStatus, null, null);
217                if (mCallback != null) {
218                    mCallback.onPlaylistChanged();
219                }
220                publishState(STATE_PAUSED);
221            }
222
223            @Override
224            public void onError(String error, int code, Bundle data) {
225                logError("pause: failed", error, code);
226            }
227        });
228    }
229
230    @Override
231    public void resume() {
232        if (!mClient.hasSession()) {
233            // ignore if no session
234            return;
235        }
236        if (DEBUG) {
237            Log.d(TAG, "resume");
238        }
239        mClient.resume(null, new SessionActionCallback() {
240            @Override
241            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
242                logStatus("resume: succeeded", sessionId, sessionStatus, null, null);
243                if (mCallback != null) {
244                    mCallback.onPlaylistChanged();
245                }
246                publishState(STATE_PLAYING);
247            }
248
249            @Override
250            public void onError(String error, int code, Bundle data) {
251                logError("resume: failed", error, code);
252            }
253        });
254    }
255
256    @Override
257    public void stop() {
258        if (!mClient.hasSession()) {
259            // ignore if no session
260            return;
261        }
262        publishState(STATE_IDLE);
263        if (DEBUG) {
264            Log.d(TAG, "stop");
265        }
266        mClient.stop(null, new SessionActionCallback() {
267            @Override
268            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
269                logStatus("stop: succeeded", sessionId, sessionStatus, null, null);
270                if (mClient.isSessionManagementSupported()) {
271                    endSession();
272                }
273                if (mCallback != null) {
274                    mCallback.onPlaylistChanged();
275                }
276            }
277
278            @Override
279            public void onError(String error, int code, Bundle data) {
280                logError("stop: failed", error, code);
281            }
282        });
283    }
284
285    // enqueue & remove are only supported if isQueuingSupported() returns true
286    @Override
287    public void enqueue(final PlaylistItem item) {
288        throwIfQueuingUnsupported();
289
290        if (!mClient.hasSession() && !mEnqueuePending) {
291            mEnqueuePending = true;
292            if (mClient.isSessionManagementSupported()) {
293                startSession(item);
294            } else {
295                enqueueInternal(item);
296            }
297        } else if (mEnqueuePending){
298            mTempQueue.add(item);
299        } else {
300            enqueueInternal(item);
301        }
302    }
303
304    @Override
305    public PlaylistItem remove(String itemId) {
306        throwIfNoSession();
307        throwIfQueuingUnsupported();
308
309        if (DEBUG) {
310            Log.d(TAG, "remove: itemId=" + itemId);
311        }
312        mClient.remove(itemId, null, new ItemActionCallback() {
313            @Override
314            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
315                    String itemId, MediaItemStatus itemStatus) {
316                logStatus("remove: succeeded", sessionId, sessionStatus, itemId, itemStatus);
317            }
318
319            @Override
320            public void onError(String error, int code, Bundle data) {
321                logError("remove: failed", error, code);
322            }
323        });
324
325        return null;
326    }
327
328    @Override
329    public void updateTrackInfo() {
330        // clear stats info first
331        mTrackInfo = "";
332        mSnapshot = null;
333
334        Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_TRACK_INFO);
335        intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE);
336
337        if (mRoute != null && mRoute.supportsControlRequest(intent)) {
338            ControlRequestCallback callback = new ControlRequestCallback() {
339                @Override
340                public void onResult(Bundle data) {
341                    if (DEBUG) {
342                        Log.d(TAG, "getStatistics: succeeded: data=" + data);
343                    }
344                    if (data != null) {
345                        mTrackInfo = data.getString(SampleMediaRouteProvider.TRACK_INFO_DESC);
346                        mSnapshot = data.getParcelable(
347                                SampleMediaRouteProvider.TRACK_INFO_SNAPSHOT);
348                    }
349                }
350
351                @Override
352                public void onError(String error, Bundle data) {
353                    Log.d(TAG, "getStatistics: failed: error=" + error + ", data=" + data);
354                }
355            };
356
357            mRoute.sendControlRequest(intent, callback);
358        }
359    }
360
361    @Override
362    public String getDescription() {
363        return mTrackInfo;
364    }
365
366    @Override
367    public Bitmap getSnapshot() {
368        return mSnapshot;
369    }
370
371    private void enqueueInternal(final PlaylistItem item) {
372        throwIfQueuingUnsupported();
373
374        if (DEBUG) {
375            Log.d(TAG, "enqueue: item=" + item);
376        }
377        mClient.enqueue(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() {
378            @Override
379            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
380                    String itemId, MediaItemStatus itemStatus) {
381                logStatus("enqueue: succeeded", sessionId, sessionStatus, itemId, itemStatus);
382                item.setRemoteItemId(itemId);
383                if (item.getPosition() > 0) {
384                    seekInternal(item);
385                }
386                if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
387                    pause();
388                }
389                if (mEnqueuePending) {
390                    mEnqueuePending = false;
391                    for (PlaylistItem item : mTempQueue) {
392                        enqueueInternal(item);
393                    }
394                    mTempQueue.clear();
395                }
396                if (mCallback != null) {
397                    mCallback.onPlaylistChanged();
398                }
399            }
400
401            @Override
402            public void onError(String error, int code, Bundle data) {
403                logError("enqueue: failed", error, code);
404                if (mCallback != null) {
405                    mCallback.onPlaylistChanged();
406                }
407            }
408        });
409    }
410
411    private void seekInternal(final PlaylistItem item) {
412        throwIfNoSession();
413
414        if (DEBUG) {
415            Log.d(TAG, "seek: item=" + item);
416        }
417        mClient.seek(item.getRemoteItemId(), item.getPosition(), null, new ItemActionCallback() {
418           @Override
419           public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
420                   String itemId, MediaItemStatus itemStatus) {
421               logStatus("seek: succeeded", sessionId, sessionStatus, itemId, itemStatus);
422               if (mCallback != null) {
423                   mCallback.onPlaylistChanged();
424               }
425           }
426
427           @Override
428           public void onError(String error, int code, Bundle data) {
429               logError("seek: failed", error, code);
430           }
431        });
432    }
433
434    private void startSession(final PlaylistItem item) {
435        mClient.startSession(null, new SessionActionCallback() {
436            @Override
437            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
438                logStatus("startSession: succeeded", sessionId, sessionStatus, null, null);
439                enqueueInternal(item);
440            }
441
442            @Override
443            public void onError(String error, int code, Bundle data) {
444                logError("startSession: failed", error, code);
445            }
446        });
447    }
448
449    private void endSession() {
450        mClient.endSession(null, new SessionActionCallback() {
451            @Override
452            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
453                logStatus("endSession: succeeded", sessionId, sessionStatus, null, null);
454            }
455
456            @Override
457            public void onError(String error, int code, Bundle data) {
458                logError("endSession: failed", error, code);
459            }
460        });
461    }
462
463    private void logStatus(String message,
464            String sessionId, MediaSessionStatus sessionStatus,
465            String itemId, MediaItemStatus itemStatus) {
466        if (DEBUG) {
467            String result = "";
468            if (sessionId != null && sessionStatus != null) {
469                result += "sessionId=" + sessionId + ", sessionStatus=" + sessionStatus;
470            }
471            if (itemId != null & itemStatus != null) {
472                result += (result.isEmpty() ? "" : ", ")
473                        + "itemId=" + itemId + ", itemStatus=" + itemStatus;
474            }
475            Log.d(TAG, message + ": " + result);
476        }
477    }
478
479    private void logError(String message, String error, int code) {
480        Log.d(TAG, message + ": error=" + error + ", code=" + code);
481    }
482
483    private void throwIfNoSession() {
484        if (!mClient.hasSession()) {
485            throw new IllegalStateException("Session is invalid");
486        }
487    }
488
489    private void throwIfQueuingUnsupported() {
490        if (!isQueuingSupported()) {
491            throw new UnsupportedOperationException("Queuing is unsupported");
492        }
493    }
494}
495