LocalRenderer.java revision bfa153b64b4e8c2faa39a15e87fc9f0300335f20
1package com.android.onemedia.playback; 2 3import org.apache.http.Header; 4import org.apache.http.HttpResponse; 5import org.apache.http.client.methods.HttpGet; 6 7import android.content.Context; 8import android.media.AudioManager; 9import android.media.AudioManager.OnAudioFocusChangeListener; 10import android.media.MediaPlayer; 11import android.media.MediaPlayer.OnBufferingUpdateListener; 12import android.media.MediaPlayer.OnCompletionListener; 13import android.media.MediaPlayer.OnErrorListener; 14import android.media.MediaPlayer.OnPreparedListener; 15import android.net.Uri; 16import android.net.http.AndroidHttpClient; 17import android.os.AsyncTask; 18import android.os.Bundle; 19import android.os.Handler; 20import android.util.Log; 21import android.view.SurfaceHolder; 22 23import java.io.IOException; 24import java.util.Map; 25 26/** 27 * Helper class for wrapping a MediaPlayer and doing a lot of the default work 28 * to play audio. This class is not currently thread safe and all calls to it 29 * should be made on the same thread. 30 */ 31public class LocalRenderer extends Renderer implements OnPreparedListener, 32 OnBufferingUpdateListener, OnCompletionListener, OnErrorListener, 33 OnAudioFocusChangeListener { 34 private static final String TAG = "MediaPlayerManager"; 35 private static final boolean DEBUG = true; 36 private static long sDebugInstanceId = 0; 37 38 private static final String[] SUPPORTED_FEATURES = { 39 FEATURE_SET_CONTENT, 40 FEATURE_SET_NEXT_CONTENT, 41 FEATURE_PLAY, 42 FEATURE_PAUSE, 43 FEATURE_NEXT, 44 FEATURE_PREVIOUS, 45 FEATURE_SEEK_TO, 46 FEATURE_STOP 47 }; 48 49 /** 50 * These are the states where it is valid to call play directly on the 51 * MediaPlayer. 52 */ 53 private static final int CAN_PLAY = STATE_READY | STATE_PAUSED | STATE_ENDED; 54 /** 55 * These are the states where we expect the MediaPlayer to be ready in the 56 * future, so we can set a flag to start playing when it is. 57 */ 58 private static final int CAN_READY_PLAY = STATE_INIT | STATE_PREPARING; 59 /** 60 * The states when it is valid to call pause on the MediaPlayer. 61 */ 62 private static final int CAN_PAUSE = STATE_PLAYING; 63 /** 64 * The states where it is valid to call seek on the MediaPlayer. 65 */ 66 private static final int CAN_SEEK = STATE_READY | STATE_PLAYING | STATE_PAUSED | STATE_ENDED; 67 /** 68 * The states where we expect the MediaPlayer to be ready in the future and 69 * can store a seek position to set later. 70 */ 71 private static final int CAN_READY_SEEK = STATE_INIT | STATE_PREPARING; 72 /** 73 * The states where it is valid to call stop on the MediaPlayer. 74 */ 75 private static final int CAN_STOP = STATE_READY | STATE_PLAYING | STATE_PAUSED | STATE_ENDED; 76 /** 77 * The states where it is valid to get the current play position and the 78 * duration from the MediaPlayer. 79 */ 80 private static final int CAN_GET_POSITION = STATE_READY | STATE_PLAYING | STATE_PAUSED; 81 82 83 84 private class PlayerContent { 85 public final String source; 86 public final Map<String, String> headers; 87 88 public PlayerContent(String source, Map<String, String> headers) { 89 this.source = source; 90 this.headers = headers; 91 } 92 } 93 94 private class AsyncErrorRetriever extends AsyncTask<HttpGet, Void, Void> { 95 private final long errorId; 96 private boolean closeHttpClient; 97 98 public AsyncErrorRetriever(long errorId) { 99 this.errorId = errorId; 100 closeHttpClient = false; 101 } 102 103 public boolean cancelRequestLocked(boolean closeHttp) { 104 closeHttpClient = closeHttp; 105 return this.cancel(false); 106 } 107 108 @Override 109 protected Void doInBackground(HttpGet[] params) { 110 synchronized (mErrorLock) { 111 if (isCancelled() || mHttpClient == null) { 112 if (mErrorRetriever == this) { 113 mErrorRetriever = null; 114 } 115 return null; 116 } 117 mSafeToCloseClient = false; 118 } 119 final PlaybackError error = new PlaybackError(); 120 try { 121 HttpResponse response = mHttpClient.execute(params[0]); 122 synchronized (mErrorLock) { 123 if (mErrorId != errorId || mError == null) { 124 // A new error has occurred, abort 125 return null; 126 } 127 error.type = mError.type; 128 error.extra = mError.extra; 129 error.errorMessage = mError.errorMessage; 130 } 131 final int code = response.getStatusLine().getStatusCode(); 132 if (code >= 300) { 133 error.extra = code; 134 } 135 final Bundle errorExtras = new Bundle(); 136 Header[] headers = response.getAllHeaders(); 137 if (headers != null && headers.length > 0) { 138 for (Header header : headers) { 139 errorExtras.putString(header.getName(), header.getValue()); 140 } 141 error.errorExtras = errorExtras; 142 } 143 } catch (IOException e) { 144 Log.e(TAG, "IOException requesting from server, unable to get more exact error"); 145 } finally { 146 synchronized (mErrorLock) { 147 mSafeToCloseClient = true; 148 if (mErrorRetriever == this) { 149 mErrorRetriever = null; 150 } 151 if (isCancelled()) { 152 if (closeHttpClient) { 153 mHttpClient.close(); 154 mHttpClient = null; 155 } 156 return null; 157 } 158 } 159 } 160 mHandler.post(new Runnable() { 161 @Override 162 public void run() { 163 synchronized (mErrorLock) { 164 if (mErrorId == errorId) { 165 setError(error.type, error.extra, error.errorExtras, null); 166 } 167 } 168 } 169 }); 170 return null; 171 } 172 } 173 174 private int mState = STATE_INIT; 175 176 private AudioManager mAudioManager; 177 private MediaPlayer mPlayer; 178 private PlayerContent mContent; 179 private MediaPlayer mNextPlayer; 180 private PlayerContent mNextContent; 181 private SurfaceHolder mHolder; 182 private SurfaceHolder.Callback mHolderCB; 183 private Context mContext; 184 185 private Handler mHandler = new Handler(); 186 187 private AndroidHttpClient mHttpClient = AndroidHttpClient.newInstance("TUQ"); 188 // The ongoing error request thread if there is one. This should only be 189 // modified while mErrorLock is held. 190 private AsyncErrorRetriever mErrorRetriever; 191 // This is set to false while a server request is being made to retrieve 192 // the current error. It should only be set while mErrorLock is held. 193 private boolean mSafeToCloseClient = true; 194 private final Object mErrorLock = new Object(); 195 // A tracking id for the current error. This should only be modified while 196 // mErrorLock is held. 197 private long mErrorId = 0; 198 // The current error state of this player. This is cleared when the state 199 // leaves an error state and set when it enters one. This should only be 200 // modified when mErrorLock is held. 201 private PlaybackError mError; 202 203 private boolean mPlayOnReady; 204 private int mSeekOnReady; 205 private boolean mHasAudioFocus; 206 private long mDebugId = sDebugInstanceId++; 207 208 public LocalRenderer(Context context, Bundle params) { 209 super(context, params); 210 mContext = context; 211 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 212 } 213 214 @Override 215 protected void initFeatures(Bundle params) { 216 for (String feature : SUPPORTED_FEATURES) { 217 mFeatures.add(feature); 218 } 219 } 220 221 /** 222 * Call this when completely finished with the MediaPlayerManager to have it 223 * clean up. The instance may not be used again after this is called. 224 */ 225 @Override 226 public void onDestroy() { 227 synchronized (mErrorLock) { 228 if (DEBUG) { 229 Log.d(TAG, "onDestroy, error retriever? " + mErrorRetriever + " safe to close? " 230 + mSafeToCloseClient + " client? " + mHttpClient); 231 } 232 if (mErrorRetriever != null) { 233 mErrorRetriever.cancelRequestLocked(true); 234 mErrorRetriever = null; 235 } 236 // Increment the error id to ensure no errors are sent after this 237 // point. 238 mErrorId++; 239 if (mSafeToCloseClient) { 240 mHttpClient.close(); 241 mHttpClient = null; 242 } 243 } 244 } 245 246 @Override 247 public void onPrepared(MediaPlayer player) { 248 if (!isCurrentPlayer(player)) { 249 return; 250 } 251 setState(STATE_READY); 252 if (DEBUG) { 253 Log.d(TAG, mDebugId + ": Finished preparing, seekOnReady is " + mSeekOnReady); 254 } 255 if (mSeekOnReady >= 0) { 256 onSeekTo(mSeekOnReady); 257 mSeekOnReady = -1; 258 } 259 if (mPlayOnReady) { 260 player.start(); 261 setState(STATE_PLAYING); 262 } 263 } 264 265 @Override 266 public void onBufferingUpdate(MediaPlayer player, int percent) { 267 if (!isCurrentPlayer(player)) { 268 return; 269 } 270 pushOnBufferingUpdate(percent); 271 } 272 273 @Override 274 public void onCompletion(MediaPlayer player) { 275 if (!isCurrentPlayer(player)) { 276 return; 277 } 278 if (DEBUG) { 279 Log.d(TAG, mDebugId + ": Completed item. Have next item? " + (mNextPlayer != null)); 280 } 281 if (mNextPlayer != null) { 282 if (mPlayer != null) { 283 mPlayer.release(); 284 } 285 mPlayer = mNextPlayer; 286 mContent = mNextContent; 287 mNextPlayer = null; 288 mNextContent = null; 289 pushOnNextStarted(); 290 return; 291 } 292 setState(STATE_ENDED); 293 } 294 295 @Override 296 public boolean onError(MediaPlayer player, int what, int extra) { 297 if (!isCurrentPlayer(player)) { 298 return false; 299 } 300 if (DEBUG) { 301 Log.d(TAG, mDebugId + ": Entered error state, what: " + what + " extra: " + extra); 302 } 303 synchronized (mErrorLock) { 304 ++mErrorId; 305 mError = new PlaybackError(); 306 mError.type = what; 307 mError.extra = extra; 308 } 309 310 if (what == MediaPlayer.MEDIA_ERROR_UNKNOWN && extra == MediaPlayer.MEDIA_ERROR_IO 311 && mContent != null && mContent.source.startsWith("http")) { 312 HttpGet request = new HttpGet(mContent.source); 313 if (mContent.headers != null) { 314 for (String key : mContent.headers.keySet()) { 315 request.addHeader(key, mContent.headers.get(key)); 316 } 317 } 318 synchronized (mErrorLock) { 319 if (mErrorRetriever != null) { 320 mErrorRetriever.cancelRequestLocked(false); 321 } 322 mErrorRetriever = new AsyncErrorRetriever(mErrorId); 323 mErrorRetriever.execute(request); 324 } 325 } else { 326 setError(what, extra, null, null); 327 } 328 return true; 329 } 330 331 @Override 332 public void onAudioFocusChange(int focusChange) { 333 // TODO figure out appropriate logic for handling focus loss at the TUQ 334 // level. 335 switch (focusChange) { 336 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 337 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 338 if (mState == STATE_PLAYING) { 339 onPause(); 340 mPlayOnReady = true; 341 } 342 mHasAudioFocus = false; 343 break; 344 case AudioManager.AUDIOFOCUS_LOSS: 345 if (mState == STATE_PLAYING) { 346 onPause(); 347 mPlayOnReady = false; 348 } 349 pushOnFocusLost(); 350 mHasAudioFocus = false; 351 break; 352 case AudioManager.AUDIOFOCUS_GAIN: 353 mHasAudioFocus = true; 354 if (mPlayOnReady) { 355 onPlay(); 356 } 357 break; 358 default: 359 Log.d(TAG, "Unknown focus change event " + focusChange); 360 break; 361 } 362 } 363 364 @Override 365 public void setContent(Bundle request) { 366 setContent(request, null); 367 } 368 369 /** 370 * Prepares the player for the given playback request. If the holder is null 371 * it is assumed this is an audio only source. If playOnReady is set to true 372 * the media will begin playing as soon as it can. 373 */ 374 public void setContent(Bundle request, SurfaceHolder holder) { 375 String source = request.getString(RequestUtils.EXTRA_KEY_SOURCE); 376 Map<String, String> headers = null; // request.mHeaders; 377 boolean playOnReady = true; // request.mPlayOnReady; 378 if (DEBUG) { 379 Log.d(TAG, mDebugId + ": Settings new content. Have a player? " + (mPlayer != null) 380 + " have a next player? " + (mNextPlayer != null)); 381 } 382 cleanUpPlayer(); 383 setState(STATE_PREPARING); 384 mPlayOnReady = playOnReady; 385 mSeekOnReady = -1; 386 final MediaPlayer newPlayer = new MediaPlayer(); 387 388 requestAudioFocus(); 389 390 mPlayer = newPlayer; 391 mContent = new PlayerContent(source, headers); 392 try { 393 if (headers != null) { 394 Uri sourceUri = Uri.parse(source); 395 newPlayer.setDataSource(mContext, sourceUri, headers); 396 } else { 397 newPlayer.setDataSource(source); 398 } 399 } catch (Exception e) { 400 setError(Listener.ERROR_LOAD_FAILED, 0, null, e); 401 return; 402 } 403 if (isHolderReady(holder, newPlayer)) { 404 preparePlayer(newPlayer, true); 405 } 406 } 407 408 @Override 409 public void setNextContent(Bundle request) { 410 String source = request.getString(RequestUtils.EXTRA_KEY_SOURCE); 411 Map<String, String> headers = null; // request.mHeaders; 412 413 // TODO support video 414 415 if (DEBUG) { 416 Log.d(TAG, mDebugId + ": Setting next content. Have player? " + (mPlayer != null) 417 + " have next player? " + (mNextPlayer != null)); 418 } 419 420 if (mPlayer == null) { 421 // The manager isn't being used to play anything, don't try to 422 // set a next. 423 return; 424 } 425 if (mNextPlayer != null) { 426 // Before setting up the new one clear out the old one and release 427 // it to ensure it doesn't play. 428 mPlayer.setNextMediaPlayer(null); 429 mNextPlayer.release(); 430 mNextPlayer = null; 431 mNextContent = null; 432 } 433 if (source == null) { 434 // If there's no new content we're done 435 return; 436 } 437 final MediaPlayer newPlayer = new MediaPlayer(); 438 439 try { 440 if (headers != null) { 441 Uri sourceUri = Uri.parse(source); 442 newPlayer.setDataSource(mContext, sourceUri, headers); 443 } else { 444 newPlayer.setDataSource(source); 445 } 446 } catch (Exception e) { 447 newPlayer.release(); 448 // Don't return an error until we get to this item in playback 449 return; 450 } 451 452 if (preparePlayer(newPlayer, false)) { 453 mPlayer.setNextMediaPlayer(newPlayer); 454 mNextPlayer = newPlayer; 455 mNextContent = new PlayerContent(source, headers); 456 } 457 } 458 459 private void requestAudioFocus() { 460 int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, 461 AudioManager.AUDIOFOCUS_GAIN); 462 mHasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED; 463 } 464 465 /** 466 * Start the player if possible or queue it to play when ready. If the 467 * player is in a state where it will never be ready returns false. 468 * 469 * @return true if the content was started or will be started later 470 */ 471 @Override 472 public boolean onPlay() { 473 MediaPlayer player = mPlayer; 474 if (player != null && mState == STATE_PLAYING) { 475 // already playing, just return 476 return true; 477 } 478 if (!mHasAudioFocus) { 479 requestAudioFocus(); 480 } 481 if (player != null && canPlay()) { 482 player.start(); 483 setState(STATE_PLAYING); 484 } else if (canReadyPlay()) { 485 mPlayOnReady = true; 486 } else if (!isPlaying()) { 487 return false; 488 } 489 return true; 490 } 491 492 /** 493 * Pause the player if possible or set it to not play when ready. If the 494 * player is in a state where it will never be ready returns false. 495 * 496 * @return true if the content was paused or will wait to play when ready 497 * later 498 */ 499 @Override 500 public boolean onPause() { 501 MediaPlayer player = mPlayer; 502 if (player != null && (mState & CAN_PAUSE) != 0) { 503 player.pause(); 504 setState(STATE_PAUSED); 505 } else if ((mState & CAN_READY_PLAY) != 0) { 506 mPlayOnReady = false; 507 } else if (!isPaused()) { 508 return false; 509 } 510 return true; 511 } 512 513 /** 514 * Seek to a given position in the media. If the seek succeeded or will be 515 * performed when loading is complete returns true. If the position is not 516 * in range or the player will never be ready returns false. 517 * 518 * @param position The position to seek to in milliseconds 519 * @return true if playback was moved or will be moved when ready 520 */ 521 @Override 522 public boolean onSeekTo(int position) { 523 MediaPlayer player = mPlayer; 524 if (player != null && (mState & CAN_SEEK) != 0) { 525 if (position < 0 || position >= getDuration()) { 526 return false; 527 } else { 528 if (mState == STATE_ENDED) { 529 player.start(); 530 player.pause(); 531 setState(STATE_PAUSED); 532 } 533 player.seekTo(position); 534 } 535 } else if ((mState & CAN_READY_SEEK) != 0) { 536 mSeekOnReady = position; 537 } else { 538 return false; 539 } 540 return true; 541 } 542 543 /** 544 * Stop the player. It cannot be used again until 545 * {@link #setContent(String, boolean)} is called. 546 * 547 * @return true if stopping the player succeeded 548 */ 549 @Override 550 public boolean onStop() { 551 cleanUpPlayer(); 552 setState(STATE_STOPPED); 553 return true; 554 } 555 556 public boolean isPlaying() { 557 return mState == STATE_PLAYING; 558 } 559 560 public boolean isPaused() { 561 return mState == STATE_PAUSED; 562 } 563 564 @Override 565 public long getSeekPosition() { 566 return ((mState & CAN_GET_POSITION) == 0) ? -1 : mPlayer.getCurrentPosition(); 567 } 568 569 @Override 570 public long getDuration() { 571 return ((mState & CAN_GET_POSITION) == 0) ? -1 : mPlayer.getDuration(); 572 } 573 574 private boolean canPlay() { 575 return ((mState & CAN_PLAY) != 0) && mHasAudioFocus; 576 } 577 578 private boolean canReadyPlay() { 579 return (mState & CAN_PLAY) != 0 || (mState & CAN_READY_PLAY) != 0; 580 } 581 582 /** 583 * Sends a state update if the listener exists 584 */ 585 private void setState(int state) { 586 if (state == mState) { 587 return; 588 } 589 Log.d(TAG, "Entering state " + state + " from state " + mState); 590 mState = state; 591 if (state != STATE_ERROR) { 592 // Don't notify error here, it'll get sent via onError 593 pushOnStateChanged(state); 594 } 595 } 596 597 private boolean preparePlayer(final MediaPlayer player, boolean current) { 598 player.setOnPreparedListener(this); 599 player.setOnBufferingUpdateListener(this); 600 player.setOnCompletionListener(this); 601 player.setOnErrorListener(this); 602 try { 603 player.prepareAsync(); 604 if (current) { 605 setState(STATE_PREPARING); 606 } 607 } catch (IllegalStateException e) { 608 if (current) { 609 setError(Listener.ERROR_PREPARE_ERROR, 0, null, e); 610 } 611 return false; 612 } 613 return true; 614 } 615 616 /** 617 * @param extra 618 * @param e 619 */ 620 private void setError(int type, int extra, Bundle extras, Exception e) { 621 setState(STATE_ERROR); 622 pushOnError(type, extra, extras, e); 623 cleanUpPlayer(); 624 return; 625 } 626 627 /** 628 * Checks if the holder is ready and either sets up a callback to wait for 629 * it or sets it directly. If 630 * 631 * @param holder 632 * @param player 633 * @return 634 */ 635 private boolean isHolderReady(final SurfaceHolder holder, final MediaPlayer player) { 636 mHolder = holder; 637 if (holder != null) { 638 if (holder.getSurface() != null && holder.getSurface().isValid()) { 639 player.setDisplay(holder); 640 return true; 641 } else { 642 Log.w(TAG, "Holder not null, waiting for it to be ready"); 643 // If the holder isn't ready yet add a callback to set the 644 // holder when it's ready. 645 SurfaceHolder.Callback cb = new SurfaceHolder.Callback() { 646 @Override 647 public void surfaceDestroyed(SurfaceHolder arg0) { 648 } 649 650 @Override 651 public void surfaceCreated(SurfaceHolder arg0) { 652 if (player.equals(mPlayer)) { 653 player.setDisplay(arg0); 654 preparePlayer(player, true); 655 } 656 } 657 658 @Override 659 public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) { 660 } 661 }; 662 mHolderCB = cb; 663 holder.addCallback(cb); 664 return false; 665 } 666 } 667 return true; 668 } 669 670 private void cleanUpPlayer() { 671 if (DEBUG) { 672 Log.d(TAG, mDebugId + ": Cleaning up current player"); 673 } 674 synchronized (mErrorLock) { 675 mError = null; 676 if (mErrorRetriever != null) { 677 mErrorRetriever.cancelRequestLocked(false); 678 // Don't set to null as we may need to cancel again with true if 679 // the object gets destroyed. 680 } 681 } 682 mAudioManager.abandonAudioFocus(this); 683 684 SurfaceHolder.Callback cb = mHolderCB; 685 mHolderCB = null; 686 SurfaceHolder holder = mHolder; 687 mHolder = null; 688 if (holder != null && cb != null) { 689 holder.removeCallback(cb); 690 } 691 692 MediaPlayer player = mPlayer; 693 mPlayer = null; 694 if (player != null) { 695 player.reset(); 696 player.release(); 697 } 698 } 699 700 private boolean isCurrentPlayer(MediaPlayer player) { 701 return player.equals(mPlayer); 702 } 703} 704