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
}