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