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