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