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