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