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.app.Activity;
20import android.app.Presentation;
21import android.content.Context;
22import android.content.DialogInterface;
23import android.graphics.Bitmap;
24import android.media.MediaPlayer;
25import android.os.Build;
26import android.os.Bundle;
27import android.os.Handler;
28import android.os.SystemClock;
29import android.util.Log;
30import android.view.Display;
31import android.view.Gravity;
32import android.view.Surface;
33import android.view.SurfaceHolder;
34import android.view.SurfaceView;
35import android.view.View;
36import android.view.ViewGroup;
37import android.view.WindowManager;
38import android.widget.FrameLayout;
39
40import androidx.annotation.RequiresApi;
41import androidx.mediarouter.media.MediaItemStatus;
42import androidx.mediarouter.media.MediaRouter.RouteInfo;
43
44import com.example.android.supportv7.R;
45
46import java.io.IOException;
47
48/**
49 * Handles playback of a single media item using MediaPlayer.
50 */
51public abstract class LocalPlayer extends Player implements
52        MediaPlayer.OnPreparedListener,
53        MediaPlayer.OnCompletionListener,
54        MediaPlayer.OnErrorListener,
55        MediaPlayer.OnSeekCompleteListener {
56    private static final String TAG = "LocalPlayer";
57    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
58
59    private final Context mContext;
60    private final Handler mHandler = new Handler();
61    private final Handler mUpdateSurfaceHandler = new Handler(mHandler.getLooper());
62    private MediaPlayer mMediaPlayer;
63    private int mState = STATE_IDLE;
64    private int mSeekToPos;
65    private int mVideoWidth;
66    private int mVideoHeight;
67    private Surface mSurface;
68    private SurfaceHolder mSurfaceHolder;
69
70    public LocalPlayer(Context context) {
71        mContext = context;
72
73        // reset media player
74        reset();
75    }
76
77    @Override
78    public boolean isRemotePlayback() {
79        return false;
80    }
81
82    @Override
83    public boolean isQueuingSupported() {
84        return false;
85    }
86
87    @Override
88    public void connect(RouteInfo route) {
89        if (DEBUG) {
90            Log.d(TAG, "connecting to: " + route);
91        }
92    }
93
94    @Override
95    public void release() {
96        if (DEBUG) {
97            Log.d(TAG, "releasing");
98        }
99        // release media player
100        if (mMediaPlayer != null) {
101            mMediaPlayer.stop();
102            mMediaPlayer.release();
103            mMediaPlayer = null;
104        }
105    }
106
107    // Player
108    @Override
109    public void play(final PlaylistItem item) {
110        if (DEBUG) {
111            Log.d(TAG, "play: item=" + item);
112        }
113        reset();
114        mSeekToPos = (int)item.getPosition();
115        try {
116            mMediaPlayer.setDataSource(mContext, item.getUri());
117            mMediaPlayer.prepareAsync();
118        } catch (IllegalStateException e) {
119            Log.e(TAG, "MediaPlayer throws IllegalStateException, uri=" + item.getUri());
120        } catch (IOException e) {
121            Log.e(TAG, "MediaPlayer throws IOException, uri=" + item.getUri());
122        } catch (IllegalArgumentException e) {
123            Log.e(TAG, "MediaPlayer throws IllegalArgumentException, uri=" + item.getUri());
124        } catch (SecurityException e) {
125            Log.e(TAG, "MediaPlayer throws SecurityException, uri=" + item.getUri());
126        }
127        if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
128            resume();
129        } else {
130            pause();
131        }
132    }
133
134    @Override
135    public void seek(final PlaylistItem item) {
136        if (DEBUG) {
137            Log.d(TAG, "seek: item=" + item);
138        }
139        int pos = (int)item.getPosition();
140        if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
141            mMediaPlayer.seekTo(pos);
142            mSeekToPos = pos;
143        } else if (mState == STATE_IDLE || mState == STATE_PREPARING_FOR_PLAY
144                || mState == STATE_PREPARING_FOR_PAUSE) {
145            // Seek before onPrepared() arrives,
146            // need to performed delayed seek in onPrepared()
147            mSeekToPos = pos;
148        }
149    }
150
151    @Override
152    public void getStatus(final PlaylistItem item, final boolean update) {
153        if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
154            // use mSeekToPos if we're currently seeking (mSeekToPos is reset
155            // when seeking is completed)
156            item.setDuration(mMediaPlayer.getDuration());
157            item.setPosition(mSeekToPos > 0 ?
158                    mSeekToPos : mMediaPlayer.getCurrentPosition());
159            item.setTimestamp(SystemClock.elapsedRealtime());
160        }
161        if (update && mCallback != null) {
162            mCallback.onPlaylistReady();
163        }
164    }
165
166    @Override
167    public void pause() {
168        if (DEBUG) {
169            Log.d(TAG, "pause");
170        }
171        if (mState == STATE_PLAYING) {
172            mMediaPlayer.pause();
173            mState = STATE_PAUSED;
174        } else if (mState == STATE_PREPARING_FOR_PLAY) {
175            mState = STATE_PREPARING_FOR_PAUSE;
176        }
177    }
178
179    @Override
180    public void resume() {
181        if (DEBUG) {
182            Log.d(TAG, "resume");
183        }
184        if (mState == STATE_READY || mState == STATE_PAUSED) {
185            mMediaPlayer.start();
186            mState = STATE_PLAYING;
187        } else if (mState == STATE_IDLE || mState == STATE_PREPARING_FOR_PAUSE) {
188            mState = STATE_PREPARING_FOR_PLAY;
189        }
190    }
191
192    @Override
193    public void stop() {
194        if (DEBUG) {
195            Log.d(TAG, "stop");
196        }
197        if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
198            mMediaPlayer.stop();
199            mState = STATE_IDLE;
200        }
201    }
202
203    @Override
204    public void enqueue(final PlaylistItem item) {
205        throw new UnsupportedOperationException("LocalPlayer doesn't support enqueue!");
206    }
207
208    @Override
209    public PlaylistItem remove(String iid) {
210        throw new UnsupportedOperationException("LocalPlayer doesn't support remove!");
211    }
212
213    //MediaPlayer Listeners
214    @Override
215    public void onPrepared(MediaPlayer mp) {
216        if (DEBUG) {
217            Log.d(TAG, "onPrepared");
218        }
219        mHandler.post(new Runnable() {
220            @Override
221            public void run() {
222                if (mState == STATE_IDLE) {
223                    mState = STATE_READY;
224                    updateVideoRect();
225                } else if (mState == STATE_PREPARING_FOR_PLAY
226                        || mState == STATE_PREPARING_FOR_PAUSE) {
227                    int prevState = mState;
228                    mState = mState == STATE_PREPARING_FOR_PLAY ? STATE_PLAYING : STATE_PAUSED;
229                    updateVideoRect();
230                    if (mSeekToPos > 0) {
231                        if (DEBUG) {
232                            Log.d(TAG, "seek to initial pos: " + mSeekToPos);
233                        }
234                        mMediaPlayer.seekTo(mSeekToPos);
235                    }
236                    if (prevState == STATE_PREPARING_FOR_PLAY) {
237                        mMediaPlayer.start();
238                    }
239                }
240                if (mCallback != null) {
241                    mCallback.onPlaylistChanged();
242                }
243            }
244        });
245    }
246
247    @Override
248    public void onCompletion(MediaPlayer mp) {
249        if (DEBUG) {
250            Log.d(TAG, "onCompletion");
251        }
252        mHandler.post(new Runnable() {
253            @Override
254            public void run() {
255                if (mCallback != null) {
256                    mCallback.onCompletion();
257                }
258            }
259        });
260    }
261
262    @Override
263    public boolean onError(MediaPlayer mp, int what, int extra) {
264        if (DEBUG) {
265            Log.d(TAG, "onError");
266        }
267        mHandler.post(new Runnable() {
268            @Override
269            public void run() {
270                if (mCallback != null) {
271                    mCallback.onError();
272                }
273            }
274        });
275        // return true so that onCompletion is not called
276        return true;
277    }
278
279    @Override
280    public void onSeekComplete(MediaPlayer mp) {
281        if (DEBUG) {
282            Log.d(TAG, "onSeekComplete");
283        }
284        mHandler.post(new Runnable() {
285            @Override
286            public void run() {
287                mSeekToPos = 0;
288                if (mCallback != null) {
289                    mCallback.onPlaylistChanged();
290                }
291            }
292        });
293    }
294
295    protected Context getContext() { return mContext; }
296    protected MediaPlayer getMediaPlayer() { return mMediaPlayer; }
297    protected int getVideoWidth() { return mVideoWidth; }
298    protected int getVideoHeight() { return mVideoHeight; }
299    protected int getState() { return mState; }
300    protected void setSurface(Surface surface) {
301        mSurface = surface;
302        mSurfaceHolder = null;
303        updateSurface();
304    }
305
306    protected void setSurface(SurfaceHolder surfaceHolder) {
307        mSurface = null;
308        mSurfaceHolder = surfaceHolder;
309        updateSurface();
310    }
311
312    protected void removeSurface(SurfaceHolder surfaceHolder) {
313        if (surfaceHolder == mSurfaceHolder) {
314            setSurface((SurfaceHolder)null);
315        }
316    }
317
318    protected void updateSurface() {
319        mUpdateSurfaceHandler.removeCallbacksAndMessages(null);
320        mUpdateSurfaceHandler.post(new Runnable() {
321            @Override
322            public void run() {
323                if (mMediaPlayer == null) {
324                    // just return if media player is already gone
325                    return;
326                }
327                if (mSurface != null) {
328                    // The setSurface API does not exist until V14+.
329                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
330                        ICSMediaPlayer.setSurface(mMediaPlayer, mSurface);
331                    } else {
332                        throw new UnsupportedOperationException("MediaPlayer does not support "
333                                + "setSurface() on this version of the platform.");
334                    }
335                } else if (mSurfaceHolder != null) {
336                    mMediaPlayer.setDisplay(mSurfaceHolder);
337                } else {
338                    mMediaPlayer.setDisplay(null);
339                }
340            }
341        });
342    }
343
344    protected abstract void updateSize();
345
346    private void reset() {
347        if (mMediaPlayer != null) {
348            mMediaPlayer.stop();
349            mMediaPlayer.release();
350            mMediaPlayer = null;
351        }
352        mMediaPlayer = new MediaPlayer();
353        mMediaPlayer.setOnPreparedListener(this);
354        mMediaPlayer.setOnCompletionListener(this);
355        mMediaPlayer.setOnErrorListener(this);
356        mMediaPlayer.setOnSeekCompleteListener(this);
357        updateSurface();
358        mState = STATE_IDLE;
359        mSeekToPos = 0;
360    }
361
362    private void updateVideoRect() {
363        if (mState != STATE_IDLE && mState != STATE_PREPARING_FOR_PLAY
364                && mState != STATE_PREPARING_FOR_PAUSE) {
365            int width = mMediaPlayer.getVideoWidth();
366            int height = mMediaPlayer.getVideoHeight();
367            if (width > 0 && height > 0) {
368                mVideoWidth = width;
369                mVideoHeight = height;
370                updateSize();
371            } else {
372                Log.e(TAG, "video rect is 0x0!");
373                mVideoWidth = mVideoHeight = 0;
374            }
375        }
376    }
377
378    private static final class ICSMediaPlayer {
379        public static 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        @RequiresApi(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        @RequiresApi(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        @RequiresApi(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 = 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