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.app.Activity;
20 import android.app.Presentation;
21 import android.content.Context;
22 import android.content.DialogInterface;
23 import android.media.MediaPlayer;
24 import android.os.Build;
25 import android.os.Bundle;
26 import android.os.Handler;
27 import android.os.SystemClock;
28 import android.support.v7.media.MediaItemStatus;
29 import android.support.v7.media.MediaRouter.RouteInfo;
30 import android.util.Log;
31 import android.view.Display;
32 import android.view.Gravity;
33 import android.view.Surface;
34 import android.view.SurfaceHolder;
35 import android.view.SurfaceView;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.view.WindowManager;
39 import android.widget.FrameLayout;
40 
41 import com.example.android.mediarouter.R;
42 
43 import java.io.IOException;
44 
45 /**
46  * Handles playback of a single media item using MediaPlayer.
47  */
48 public abstract class LocalPlayer extends Player implements
49         MediaPlayer.OnPreparedListener,
50         MediaPlayer.OnCompletionListener,
51         MediaPlayer.OnErrorListener,
52         MediaPlayer.OnSeekCompleteListener {
53     private static final String TAG = "LocalPlayer";
54     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
55 
56     private static final int STATE_IDLE = 0;
57     private static final int STATE_PLAY_PENDING = 1;
58     private static final int STATE_READY = 2;
59     private static final int STATE_PLAYING = 3;
60     private static final int STATE_PAUSED = 4;
61 
62     private final Context mContext;
63     private final Handler mHandler = new Handler();
64     private MediaPlayer mMediaPlayer;
65     private int mState = STATE_IDLE;
66     private int mSeekToPos;
67     private int mVideoWidth;
68     private int mVideoHeight;
69     private Surface mSurface;
70     private SurfaceHolder mSurfaceHolder;
71 
LocalPlayer(Context context)72     public LocalPlayer(Context context) {
73         mContext = context;
74 
75         // reset media player
76         reset();
77     }
78 
79     @Override
isRemotePlayback()80     public boolean isRemotePlayback() {
81         return false;
82     }
83 
84     @Override
isQueuingSupported()85     public boolean isQueuingSupported() {
86         return false;
87     }
88 
89     @Override
connect(RouteInfo route)90     public void connect(RouteInfo route) {
91         if (DEBUG) {
92             Log.d(TAG, "connecting to: " + route);
93         }
94     }
95 
96     @Override
release()97     public void release() {
98         if (DEBUG) {
99             Log.d(TAG, "releasing");
100         }
101         // release media player
102         if (mMediaPlayer != null) {
103             mMediaPlayer.stop();
104             mMediaPlayer.release();
105             mMediaPlayer = null;
106         }
107     }
108 
109     // Player
110     @Override
play(final PlaylistItem item)111     public void play(final PlaylistItem item) {
112         if (DEBUG) {
113             Log.d(TAG, "play: item=" + item);
114         }
115         reset();
116         mSeekToPos = (int)item.getPosition();
117         try {
118             mMediaPlayer.setDataSource(mContext, item.getUri());
119             mMediaPlayer.prepareAsync();
120         } catch (IllegalStateException e) {
121             Log.e(TAG, "MediaPlayer throws IllegalStateException, uri=" + item.getUri());
122         } catch (IOException e) {
123             Log.e(TAG, "MediaPlayer throws IOException, uri=" + item.getUri());
124         } catch (IllegalArgumentException e) {
125             Log.e(TAG, "MediaPlayer throws IllegalArgumentException, uri=" + item.getUri());
126         } catch (SecurityException e) {
127             Log.e(TAG, "MediaPlayer throws SecurityException, uri=" + item.getUri());
128         }
129         if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
130             resume();
131         } else {
132             pause();
133         }
134     }
135 
136     @Override
seek(final PlaylistItem item)137     public void seek(final PlaylistItem item) {
138         if (DEBUG) {
139             Log.d(TAG, "seek: item=" + item);
140         }
141         int pos = (int)item.getPosition();
142         if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
143             mMediaPlayer.seekTo(pos);
144             mSeekToPos = pos;
145         } else if (mState == STATE_IDLE || mState == STATE_PLAY_PENDING) {
146             // Seek before onPrepared() arrives,
147             // need to performed delayed seek in onPrepared()
148             mSeekToPos = pos;
149         }
150     }
151 
152     @Override
getStatus(final PlaylistItem item, final boolean update)153     public void getStatus(final PlaylistItem item, final boolean update) {
154         if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
155             // use mSeekToPos if we're currently seeking (mSeekToPos is reset
156             // when seeking is completed)
157             item.setDuration(mMediaPlayer.getDuration());
158             item.setPosition(mSeekToPos > 0 ?
159                     mSeekToPos : mMediaPlayer.getCurrentPosition());
160             item.setTimestamp(SystemClock.elapsedRealtime());
161         }
162         if (update && mCallback != null) {
163             mCallback.onPlaylistReady();
164         }
165     }
166 
167     @Override
pause()168     public void pause() {
169         if (DEBUG) {
170             Log.d(TAG, "pause");
171         }
172         if (mState == STATE_PLAYING) {
173             mMediaPlayer.pause();
174             mState = STATE_PAUSED;
175         }
176     }
177 
178     @Override
resume()179     public void resume() {
180         if (DEBUG) {
181             Log.d(TAG, "resume");
182         }
183         if (mState == STATE_READY || mState == STATE_PAUSED) {
184             mMediaPlayer.start();
185             mState = STATE_PLAYING;
186         } else if (mState == STATE_IDLE){
187             mState = STATE_PLAY_PENDING;
188         }
189     }
190 
191     @Override
stop()192     public void stop() {
193         if (DEBUG) {
194             Log.d(TAG, "stop");
195         }
196         if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
197             mMediaPlayer.stop();
198             mState = STATE_IDLE;
199         }
200     }
201 
202     @Override
enqueue(final PlaylistItem item)203     public void enqueue(final PlaylistItem item) {
204         throw new UnsupportedOperationException("LocalPlayer doesn't support enqueue!");
205     }
206 
207     @Override
remove(String iid)208     public PlaylistItem remove(String iid) {
209         throw new UnsupportedOperationException("LocalPlayer doesn't support remove!");
210     }
211 
212     //MediaPlayer Listeners
213     @Override
onPrepared(MediaPlayer mp)214     public void onPrepared(MediaPlayer mp) {
215         if (DEBUG) {
216             Log.d(TAG, "onPrepared");
217         }
218         mHandler.post(new Runnable() {
219             @Override
220             public void run() {
221                 if (mState == STATE_IDLE) {
222                     mState = STATE_READY;
223                     updateVideoRect();
224                 } else if (mState == STATE_PLAY_PENDING) {
225                     mState = STATE_PLAYING;
226                     updateVideoRect();
227                     if (mSeekToPos > 0) {
228                         if (DEBUG) {
229                             Log.d(TAG, "seek to initial pos: " + mSeekToPos);
230                         }
231                         mMediaPlayer.seekTo(mSeekToPos);
232                     }
233                     mMediaPlayer.start();
234                 }
235                 if (mCallback != null) {
236                     mCallback.onPlaylistChanged();
237                 }
238             }
239         });
240     }
241 
242     @Override
onCompletion(MediaPlayer mp)243     public void onCompletion(MediaPlayer mp) {
244         if (DEBUG) {
245             Log.d(TAG, "onCompletion");
246         }
247         mHandler.post(new Runnable() {
248             @Override
249             public void run() {
250                 if (mCallback != null) {
251                     mCallback.onCompletion();
252                 }
253             }
254         });
255     }
256 
257     @Override
onError(MediaPlayer mp, int what, int extra)258     public boolean onError(MediaPlayer mp, int what, int extra) {
259         if (DEBUG) {
260             Log.d(TAG, "onError");
261         }
262         mHandler.post(new Runnable() {
263             @Override
264             public void run() {
265                 if (mCallback != null) {
266                     mCallback.onError();
267                 }
268             }
269         });
270         // return true so that onCompletion is not called
271         return true;
272     }
273 
274     @Override
onSeekComplete(MediaPlayer mp)275     public void onSeekComplete(MediaPlayer mp) {
276         if (DEBUG) {
277             Log.d(TAG, "onSeekComplete");
278         }
279         mHandler.post(new Runnable() {
280             @Override
281             public void run() {
282                 mSeekToPos = 0;
283                 if (mCallback != null) {
284                     mCallback.onPlaylistChanged();
285                 }
286             }
287         });
288     }
289 
getContext()290     protected Context getContext() { return mContext; }
getMediaPlayer()291     protected MediaPlayer getMediaPlayer() { return mMediaPlayer; }
getVideoWidth()292     protected int getVideoWidth() { return mVideoWidth; }
getVideoHeight()293     protected int getVideoHeight() { return mVideoHeight; }
setSurface(Surface surface)294     protected void setSurface(Surface surface) {
295         mSurface = surface;
296         mSurfaceHolder = null;
297         updateSurface();
298     }
299 
setSurface(SurfaceHolder surfaceHolder)300     protected void setSurface(SurfaceHolder surfaceHolder) {
301         mSurface = null;
302         mSurfaceHolder = surfaceHolder;
303         updateSurface();
304     }
305 
removeSurface(SurfaceHolder surfaceHolder)306     protected void removeSurface(SurfaceHolder surfaceHolder) {
307         if (surfaceHolder == mSurfaceHolder) {
308             setSurface((SurfaceHolder)null);
309         }
310     }
311 
updateSurface()312     protected void updateSurface() {
313         if (mMediaPlayer == null) {
314             // just return if media player is already gone
315             return;
316         }
317         if (mSurface != null) {
318             // The setSurface API does not exist until V14+.
319             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
320                 ICSMediaPlayer.setSurface(mMediaPlayer, mSurface);
321             } else {
322                 throw new UnsupportedOperationException("MediaPlayer does not support "
323                         + "setSurface() on this version of the platform.");
324             }
325         } else if (mSurfaceHolder != null) {
326             mMediaPlayer.setDisplay(mSurfaceHolder);
327         } else {
328             mMediaPlayer.setDisplay(null);
329         }
330     }
331 
updateSize()332     protected abstract void updateSize();
333 
reset()334     private void reset() {
335         if (mMediaPlayer != null) {
336             mMediaPlayer.stop();
337             mMediaPlayer.release();
338             mMediaPlayer = null;
339         }
340         mMediaPlayer = new MediaPlayer();
341         mMediaPlayer.setOnPreparedListener(this);
342         mMediaPlayer.setOnCompletionListener(this);
343         mMediaPlayer.setOnErrorListener(this);
344         mMediaPlayer.setOnSeekCompleteListener(this);
345         updateSurface();
346         mState = STATE_IDLE;
347         mSeekToPos = 0;
348     }
349 
updateVideoRect()350     private void updateVideoRect() {
351         if (mState != STATE_IDLE && mState != STATE_PLAY_PENDING) {
352             int width = mMediaPlayer.getVideoWidth();
353             int height = mMediaPlayer.getVideoHeight();
354             if (width > 0 && height > 0) {
355                 mVideoWidth = width;
356                 mVideoHeight = height;
357                 updateSize();
358             } else {
359                 Log.e(TAG, "video rect is 0x0!");
360                 mVideoWidth = mVideoHeight = 0;
361             }
362         }
363     }
364 
365     private static final class ICSMediaPlayer {
setSurface(MediaPlayer player, Surface surface)366         public static final void setSurface(MediaPlayer player, Surface surface) {
367             player.setSurface(surface);
368         }
369     }
370 
371     /**
372      * Handles playback of a single media item using MediaPlayer in SurfaceView
373      */
374     public static class SurfaceViewPlayer extends LocalPlayer implements
375             SurfaceHolder.Callback {
376         private static final String TAG = "SurfaceViewPlayer";
377         private RouteInfo mRoute;
378         private final SurfaceView mSurfaceView;
379         private final FrameLayout mLayout;
380         private DemoPresentation mPresentation;
381 
SurfaceViewPlayer(Context context)382         public SurfaceViewPlayer(Context context) {
383             super(context);
384 
385             mLayout = (FrameLayout)((Activity)context).findViewById(R.id.player);
386             mSurfaceView = (SurfaceView)((Activity)context).findViewById(R.id.surface_view);
387 
388             // add surface holder callback
389             SurfaceHolder holder = mSurfaceView.getHolder();
390             holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
391             holder.addCallback(this);
392         }
393 
394         @Override
connect(RouteInfo route)395         public void connect(RouteInfo route) {
396             super.connect(route);
397             mRoute = route;
398         }
399 
400         @Override
release()401         public void release() {
402             super.release();
403 
404             // dismiss presentation display
405             if (mPresentation != null) {
406                 Log.i(TAG, "Dismissing presentation because the activity is no longer visible.");
407                 mPresentation.dismiss();
408                 mPresentation = null;
409             }
410 
411             // remove surface holder callback
412             SurfaceHolder holder = mSurfaceView.getHolder();
413             holder.removeCallback(this);
414 
415             // hide the surface view when SurfaceViewPlayer is destroyed
416             mSurfaceView.setVisibility(View.GONE);
417             mLayout.setVisibility(View.GONE);
418         }
419 
420         @Override
updatePresentation()421         public void updatePresentation() {
422             // Get the current route and its presentation display.
423             Display presentationDisplay = mRoute != null ? mRoute.getPresentationDisplay() : null;
424 
425             // Dismiss the current presentation if the display has changed.
426             if (mPresentation != null && mPresentation.getDisplay() != presentationDisplay) {
427                 Log.i(TAG, "Dismissing presentation because the current route no longer "
428                         + "has a presentation display.");
429                 mPresentation.dismiss();
430                 mPresentation = null;
431             }
432 
433             // Show a new presentation if needed.
434             if (mPresentation == null && presentationDisplay != null) {
435                 Log.i(TAG, "Showing presentation on display: " + presentationDisplay);
436                 mPresentation = new DemoPresentation(getContext(), presentationDisplay);
437                 mPresentation.setOnDismissListener(mOnDismissListener);
438                 try {
439                     mPresentation.show();
440                 } catch (WindowManager.InvalidDisplayException ex) {
441                     Log.w(TAG, "Couldn't show presentation!  Display was removed in "
442                               + "the meantime.", ex);
443                     mPresentation = null;
444                 }
445             }
446 
447             updateContents();
448         }
449 
450         // SurfaceHolder.Callback
451         @Override
surfaceChanged(SurfaceHolder holder, int format, int width, int height)452         public void surfaceChanged(SurfaceHolder holder, int format,
453                 int width, int height) {
454             if (DEBUG) {
455                 Log.d(TAG, "surfaceChanged: " + width + "x" + height);
456             }
457             setSurface(holder);
458         }
459 
460         @Override
surfaceCreated(SurfaceHolder holder)461         public void surfaceCreated(SurfaceHolder holder) {
462             if (DEBUG) {
463                 Log.d(TAG, "surfaceCreated");
464             }
465             setSurface(holder);
466             updateSize();
467         }
468 
469         @Override
surfaceDestroyed(SurfaceHolder holder)470         public void surfaceDestroyed(SurfaceHolder holder) {
471             if (DEBUG) {
472                 Log.d(TAG, "surfaceDestroyed");
473             }
474             removeSurface(holder);
475         }
476 
477         @Override
updateSize()478         protected void updateSize() {
479             int width = getVideoWidth();
480             int height = getVideoHeight();
481             if (width > 0 && height > 0) {
482                 if (mPresentation == null) {
483                     int surfaceWidth = mLayout.getWidth();
484                     int surfaceHeight = mLayout.getHeight();
485 
486                     // Calculate the new size of mSurfaceView, so that video is centered
487                     // inside the framelayout with proper letterboxing/pillarboxing
488                     ViewGroup.LayoutParams lp = mSurfaceView.getLayoutParams();
489                     if (surfaceWidth * height < surfaceHeight * width) {
490                         // Black bars on top&bottom, mSurfaceView has full layout width,
491                         // while height is derived from video's aspect ratio
492                         lp.width = surfaceWidth;
493                         lp.height = surfaceWidth * height / width;
494                     } else {
495                         // Black bars on left&right, mSurfaceView has full layout height,
496                         // while width is derived from video's aspect ratio
497                         lp.width = surfaceHeight * width / height;
498                         lp.height = surfaceHeight;
499                     }
500                     Log.i(TAG, "video rect is " + lp.width + "x" + lp.height);
501                     mSurfaceView.setLayoutParams(lp);
502                 } else {
503                     mPresentation.updateSize(width, height);
504                 }
505             }
506         }
507 
updateContents()508         private void updateContents() {
509             // Show either the content in the main activity or the content in the presentation
510             if (mPresentation != null) {
511                 mLayout.setVisibility(View.GONE);
512                 mSurfaceView.setVisibility(View.GONE);
513             } else {
514                 mLayout.setVisibility(View.VISIBLE);
515                 mSurfaceView.setVisibility(View.VISIBLE);
516             }
517         }
518 
519         // Listens for when presentations are dismissed.
520         private final DialogInterface.OnDismissListener mOnDismissListener =
521                 new DialogInterface.OnDismissListener() {
522             @Override
523             public void onDismiss(DialogInterface dialog) {
524                 if (dialog == mPresentation) {
525                     Log.i(TAG, "Presentation dismissed.");
526                     mPresentation = null;
527                     updateContents();
528                 }
529             }
530         };
531 
532         // Presentation
533         private final class DemoPresentation extends Presentation {
534             private SurfaceView mPresentationSurfaceView;
535 
DemoPresentation(Context context, Display display)536             public DemoPresentation(Context context, Display display) {
537                 super(context, display);
538             }
539 
540             @Override
onCreate(Bundle savedInstanceState)541             protected void onCreate(Bundle savedInstanceState) {
542                 // Be sure to call the super class.
543                 super.onCreate(savedInstanceState);
544 
545                 // Inflate the layout.
546                 setContentView(R.layout.sample_media_router_presentation);
547 
548                 // Set up the surface view.
549                 mPresentationSurfaceView = (SurfaceView)findViewById(R.id.surface_view);
550                 SurfaceHolder holder = mPresentationSurfaceView.getHolder();
551                 holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
552                 holder.addCallback(SurfaceViewPlayer.this);
553                 Log.i(TAG, "Presentation created");
554             }
555 
updateSize(int width, int height)556             public void updateSize(int width, int height) {
557                 int surfaceHeight = getWindow().getDecorView().getHeight();
558                 int surfaceWidth = getWindow().getDecorView().getWidth();
559                 ViewGroup.LayoutParams lp = mPresentationSurfaceView.getLayoutParams();
560                 if (surfaceWidth * height < surfaceHeight * width) {
561                     lp.width = surfaceWidth;
562                     lp.height = surfaceWidth * height / width;
563                 } else {
564                     lp.width = surfaceHeight * width / height;
565                     lp.height = surfaceHeight;
566                 }
567                 Log.i(TAG, "Presentation video rect is " + lp.width + "x" + lp.height);
568                 mPresentationSurfaceView.setLayoutParams(lp);
569             }
570         }
571     }
572 
573     /**
574      * Handles playback of a single media item using MediaPlayer in
575      * OverlayDisplayWindow.
576      */
577     public static class OverlayPlayer extends LocalPlayer implements
578             OverlayDisplayWindow.OverlayWindowListener {
579         private static final String TAG = "OverlayPlayer";
580         private final OverlayDisplayWindow mOverlay;
581 
OverlayPlayer(Context context)582         public OverlayPlayer(Context context) {
583             super(context);
584 
585             mOverlay = OverlayDisplayWindow.create(getContext(),
586                     getContext().getResources().getString(
587                             R.string.sample_media_route_provider_remote),
588                     1024, 768, Gravity.CENTER);
589 
590             mOverlay.setOverlayWindowListener(this);
591         }
592 
593         @Override
connect(RouteInfo route)594         public void connect(RouteInfo route) {
595             super.connect(route);
596             mOverlay.show();
597         }
598 
599         @Override
release()600         public void release() {
601             super.release();
602             mOverlay.dismiss();
603         }
604 
605         @Override
updateSize()606         protected void updateSize() {
607             int width = getVideoWidth();
608             int height = getVideoHeight();
609             if (width > 0 && height > 0) {
610                 mOverlay.updateAspectRatio(width, height);
611             }
612         }
613 
614         // OverlayDisplayWindow.OverlayWindowListener
615         @Override
onWindowCreated(Surface surface)616         public void onWindowCreated(Surface surface) {
617             setSurface(surface);
618         }
619 
620         @Override
onWindowCreated(SurfaceHolder surfaceHolder)621         public void onWindowCreated(SurfaceHolder surfaceHolder) {
622             setSurface(surfaceHolder);
623         }
624 
625         @Override
onWindowDestroyed()626         public void onWindowDestroyed() {
627             setSurface((SurfaceHolder)null);
628         }
629     }
630 }
631