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