MediaRouter / src / com.example.android.mediarouter / player /

RemotePlayer.java

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
 
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
                }
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
            }
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
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
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
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
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
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
355
    public String getStatistics() {
356
        return mStatsInfo;
357
    }
358
 
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
 
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
 
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
 
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
 
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
 
467
    private void logError(String message, String error, int code) {
468
        Log.d(TAG, message + ": error=" + error + ", code=" + code);
469
    }
470
 
471
    private void throwIfNoSession() {
472
        if (!mClient.hasSession()) {
473
            throw new IllegalStateException("Session is invalid");
474
        }
475
    }
476
 
477
    private void throwIfQueuingUnsupported() {
478
        if (!isQueuingSupported()) {
479
            throw new UnsupportedOperationException("Queuing is unsupported");
480
        }
481
    }
482
}