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