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