| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.onemedia.playback; |
| |
| import org.apache.http.Header; |
| import org.apache.http.HttpResponse; |
| import org.apache.http.client.methods.HttpGet; |
| |
| import android.content.Context; |
| import android.media.AudioManager; |
| import android.media.AudioManager.OnAudioFocusChangeListener; |
| import android.media.MediaPlayer; |
| import android.media.MediaPlayer.OnBufferingUpdateListener; |
| import android.media.MediaPlayer.OnCompletionListener; |
| import android.media.MediaPlayer.OnErrorListener; |
| import android.media.MediaPlayer.OnPreparedListener; |
| import android.net.Uri; |
| import android.net.http.AndroidHttpClient; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.util.Log; |
| import android.view.SurfaceHolder; |
| |
| import java.io.IOException; |
| import java.util.Map; |
| |
| /** |
| * Helper class for wrapping a MediaPlayer and doing a lot of the default work |
| * to play audio. This class is not currently thread safe and all calls to it |
| * should be made on the same thread. |
| */ |
| public class LocalRenderer extends Renderer implements OnPreparedListener, |
| OnBufferingUpdateListener, OnCompletionListener, OnErrorListener, |
| OnAudioFocusChangeListener { |
| private static final String TAG = "MediaPlayerManager"; |
| private static final boolean DEBUG = false; |
| private static long sDebugInstanceId = 0; |
| |
| private static final String[] SUPPORTED_FEATURES = { |
| FEATURE_SET_CONTENT, |
| FEATURE_SET_NEXT_CONTENT, |
| FEATURE_PLAY, |
| FEATURE_PAUSE, |
| FEATURE_NEXT, |
| FEATURE_PREVIOUS, |
| FEATURE_SEEK_TO, |
| FEATURE_STOP |
| }; |
| |
| /** |
| * These are the states where it is valid to call play directly on the |
| * MediaPlayer. |
| */ |
| private static final int CAN_PLAY = STATE_READY | STATE_PAUSED | STATE_ENDED; |
| /** |
| * These are the states where we expect the MediaPlayer to be ready in the |
| * future, so we can set a flag to start playing when it is. |
| */ |
| private static final int CAN_READY_PLAY = STATE_INIT | STATE_PREPARING; |
| /** |
| * The states when it is valid to call pause on the MediaPlayer. |
| */ |
| private static final int CAN_PAUSE = STATE_PLAYING; |
| /** |
| * The states where it is valid to call seek on the MediaPlayer. |
| */ |
| private static final int CAN_SEEK = STATE_READY | STATE_PLAYING | STATE_PAUSED | STATE_ENDED; |
| /** |
| * The states where we expect the MediaPlayer to be ready in the future and |
| * can store a seek position to set later. |
| */ |
| private static final int CAN_READY_SEEK = STATE_INIT | STATE_PREPARING; |
| /** |
| * The states where it is valid to call stop on the MediaPlayer. |
| */ |
| private static final int CAN_STOP = STATE_READY | STATE_PLAYING | STATE_PAUSED | STATE_ENDED; |
| /** |
| * The states where it is valid to get the current play position and the |
| * duration from the MediaPlayer. |
| */ |
| private static final int CAN_GET_POSITION = STATE_READY | STATE_PLAYING | STATE_PAUSED; |
| |
| |
| |
| private class PlayerContent { |
| public final String source; |
| public final Map<String, String> headers; |
| |
| public PlayerContent(String source, Map<String, String> headers) { |
| this.source = source; |
| this.headers = headers; |
| } |
| } |
| |
| private class AsyncErrorRetriever extends AsyncTask<HttpGet, Void, Void> { |
| private final long errorId; |
| private boolean closeHttpClient; |
| |
| public AsyncErrorRetriever(long errorId) { |
| this.errorId = errorId; |
| closeHttpClient = false; |
| } |
| |
| public boolean cancelRequestLocked(boolean closeHttp) { |
| closeHttpClient = closeHttp; |
| return this.cancel(false); |
| } |
| |
| @Override |
| protected Void doInBackground(HttpGet[] params) { |
| synchronized (mErrorLock) { |
| if (isCancelled() || mHttpClient == null) { |
| if (mErrorRetriever == this) { |
| mErrorRetriever = null; |
| } |
| return null; |
| } |
| mSafeToCloseClient = false; |
| } |
| final PlaybackError error = new PlaybackError(); |
| try { |
| HttpResponse response = mHttpClient.execute(params[0]); |
| synchronized (mErrorLock) { |
| if (mErrorId != errorId || mError == null) { |
| // A new error has occurred, abort |
| return null; |
| } |
| error.type = mError.type; |
| error.extra = mError.extra; |
| error.errorMessage = mError.errorMessage; |
| } |
| final int code = response.getStatusLine().getStatusCode(); |
| if (code >= 300) { |
| error.extra = code; |
| } |
| final Bundle errorExtras = new Bundle(); |
| Header[] headers = response.getAllHeaders(); |
| if (headers != null && headers.length > 0) { |
| for (Header header : headers) { |
| errorExtras.putString(header.getName(), header.getValue()); |
| } |
| error.errorExtras = errorExtras; |
| } |
| } catch (IOException e) { |
| Log.e(TAG, "IOException requesting from server, unable to get more exact error"); |
| } finally { |
| synchronized (mErrorLock) { |
| mSafeToCloseClient = true; |
| if (mErrorRetriever == this) { |
| mErrorRetriever = null; |
| } |
| if (isCancelled()) { |
| if (closeHttpClient) { |
| mHttpClient.close(); |
| mHttpClient = null; |
| } |
| return null; |
| } |
| } |
| } |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| synchronized (mErrorLock) { |
| if (mErrorId == errorId) { |
| setError(error.type, error.extra, error.errorExtras, null); |
| } |
| } |
| } |
| }); |
| return null; |
| } |
| } |
| |
| private int mState = STATE_INIT; |
| |
| private AudioManager mAudioManager; |
| private MediaPlayer mPlayer; |
| private PlayerContent mContent; |
| private MediaPlayer mNextPlayer; |
| private PlayerContent mNextContent; |
| private SurfaceHolder mHolder; |
| private SurfaceHolder.Callback mHolderCB; |
| private Context mContext; |
| |
| private Handler mHandler = new Handler(); |
| |
| private AndroidHttpClient mHttpClient = AndroidHttpClient.newInstance("TUQ"); |
| // The ongoing error request thread if there is one. This should only be |
| // modified while mErrorLock is held. |
| private AsyncErrorRetriever mErrorRetriever; |
| // This is set to false while a server request is being made to retrieve |
| // the current error. It should only be set while mErrorLock is held. |
| private boolean mSafeToCloseClient = true; |
| private final Object mErrorLock = new Object(); |
| // A tracking id for the current error. This should only be modified while |
| // mErrorLock is held. |
| private long mErrorId = 0; |
| // The current error state of this player. This is cleared when the state |
| // leaves an error state and set when it enters one. This should only be |
| // modified when mErrorLock is held. |
| private PlaybackError mError; |
| |
| private boolean mPlayOnReady; |
| private int mSeekOnReady; |
| private boolean mHasAudioFocus; |
| private long mDebugId = sDebugInstanceId++; |
| |
| public LocalRenderer(Context context, Bundle params) { |
| super(context, params); |
| mContext = context; |
| mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); |
| } |
| |
| @Override |
| protected void initFeatures(Bundle params) { |
| for (String feature : SUPPORTED_FEATURES) { |
| mFeatures.add(feature); |
| } |
| } |
| |
| /** |
| * Call this when completely finished with the MediaPlayerManager to have it |
| * clean up. The instance may not be used again after this is called. |
| */ |
| @Override |
| public void onDestroy() { |
| synchronized (mErrorLock) { |
| if (DEBUG) { |
| Log.d(TAG, "onDestroy, error retriever? " + mErrorRetriever + " safe to close? " |
| + mSafeToCloseClient + " client? " + mHttpClient); |
| } |
| if (mErrorRetriever != null) { |
| mErrorRetriever.cancelRequestLocked(true); |
| mErrorRetriever = null; |
| } |
| // Increment the error id to ensure no errors are sent after this |
| // point. |
| mErrorId++; |
| if (mSafeToCloseClient) { |
| mHttpClient.close(); |
| mHttpClient = null; |
| } |
| } |
| } |
| |
| @Override |
| public void onPrepared(MediaPlayer player) { |
| if (!isCurrentPlayer(player)) { |
| return; |
| } |
| setState(STATE_READY); |
| if (DEBUG) { |
| Log.d(TAG, mDebugId + ": Finished preparing, seekOnReady is " + mSeekOnReady); |
| } |
| if (mSeekOnReady >= 0) { |
| onSeekTo(mSeekOnReady); |
| mSeekOnReady = -1; |
| } |
| if (mPlayOnReady) { |
| player.start(); |
| setState(STATE_PLAYING); |
| } |
| } |
| |
| @Override |
| public void onBufferingUpdate(MediaPlayer player, int percent) { |
| if (!isCurrentPlayer(player)) { |
| return; |
| } |
| pushOnBufferingUpdate(percent); |
| } |
| |
| @Override |
| public void onCompletion(MediaPlayer player) { |
| if (!isCurrentPlayer(player)) { |
| return; |
| } |
| if (DEBUG) { |
| Log.d(TAG, mDebugId + ": Completed item. Have next item? " + (mNextPlayer != null)); |
| } |
| if (mNextPlayer != null) { |
| if (mPlayer != null) { |
| mPlayer.release(); |
| } |
| mPlayer = mNextPlayer; |
| mContent = mNextContent; |
| mNextPlayer = null; |
| mNextContent = null; |
| pushOnNextStarted(); |
| return; |
| } |
| setState(STATE_ENDED); |
| } |
| |
| @Override |
| public boolean onError(MediaPlayer player, int what, int extra) { |
| if (!isCurrentPlayer(player)) { |
| return false; |
| } |
| if (DEBUG) { |
| Log.d(TAG, mDebugId + ": Entered error state, what: " + what + " extra: " + extra); |
| } |
| synchronized (mErrorLock) { |
| ++mErrorId; |
| mError = new PlaybackError(); |
| mError.type = what; |
| mError.extra = extra; |
| } |
| |
| if (what == MediaPlayer.MEDIA_ERROR_UNKNOWN && extra == MediaPlayer.MEDIA_ERROR_IO |
| && mContent != null && mContent.source.startsWith("http")) { |
| HttpGet request = new HttpGet(mContent.source); |
| if (mContent.headers != null) { |
| for (String key : mContent.headers.keySet()) { |
| request.addHeader(key, mContent.headers.get(key)); |
| } |
| } |
| synchronized (mErrorLock) { |
| if (mErrorRetriever != null) { |
| mErrorRetriever.cancelRequestLocked(false); |
| } |
| mErrorRetriever = new AsyncErrorRetriever(mErrorId); |
| mErrorRetriever.execute(request); |
| } |
| } else { |
| setError(what, extra, null, null); |
| } |
| return true; |
| } |
| |
| @Override |
| public void onAudioFocusChange(int focusChange) { |
| // TODO figure out appropriate logic for handling focus loss at the TUQ |
| // level. |
| switch (focusChange) { |
| case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: |
| case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: |
| if (mState == STATE_PLAYING) { |
| onPause(); |
| mPlayOnReady = true; |
| } |
| mHasAudioFocus = false; |
| break; |
| case AudioManager.AUDIOFOCUS_LOSS: |
| if (mState == STATE_PLAYING) { |
| onPause(); |
| mPlayOnReady = false; |
| } |
| pushOnFocusLost(); |
| mHasAudioFocus = false; |
| break; |
| case AudioManager.AUDIOFOCUS_GAIN: |
| mHasAudioFocus = true; |
| if (mPlayOnReady) { |
| onPlay(); |
| } |
| break; |
| default: |
| Log.d(TAG, "Unknown focus change event " + focusChange); |
| break; |
| } |
| } |
| |
| @Override |
| public void setContent(Bundle request) { |
| setContent(request, null); |
| } |
| |
| /** |
| * Prepares the player for the given playback request. If the holder is null |
| * it is assumed this is an audio only source. If playOnReady is set to true |
| * the media will begin playing as soon as it can. |
| * |
| * @see RequestUtils for the set of valid keys. |
| */ |
| public void setContent(Bundle request, SurfaceHolder holder) { |
| String source = request.getString(RequestUtils.EXTRA_KEY_SOURCE); |
| Map<String, String> headers = null; // request.mHeaders; |
| boolean playOnReady = true; // request.mPlayOnReady; |
| if (DEBUG) { |
| Log.d(TAG, mDebugId + ": Settings new content. Have a player? " + (mPlayer != null) |
| + " have a next player? " + (mNextPlayer != null)); |
| } |
| cleanUpPlayer(); |
| setState(STATE_PREPARING); |
| mPlayOnReady = playOnReady; |
| mSeekOnReady = -1; |
| final MediaPlayer newPlayer = new MediaPlayer(); |
| |
| requestAudioFocus(); |
| |
| mPlayer = newPlayer; |
| mContent = new PlayerContent(source, headers); |
| try { |
| if (headers != null) { |
| Uri sourceUri = Uri.parse(source); |
| newPlayer.setDataSource(mContext, sourceUri, headers); |
| } else { |
| newPlayer.setDataSource(source); |
| } |
| } catch (Exception e) { |
| setError(Listener.ERROR_LOAD_FAILED, 0, null, e); |
| return; |
| } |
| if (isHolderReady(holder, newPlayer)) { |
| preparePlayer(newPlayer, true); |
| } |
| } |
| |
| @Override |
| public void setNextContent(Bundle request) { |
| String source = request.getString(RequestUtils.EXTRA_KEY_SOURCE); |
| Map<String, String> headers = null; // request.mHeaders; |
| |
| // TODO support video |
| |
| if (DEBUG) { |
| Log.d(TAG, mDebugId + ": Setting next content. Have player? " + (mPlayer != null) |
| + " have next player? " + (mNextPlayer != null)); |
| } |
| |
| if (mPlayer == null) { |
| // The manager isn't being used to play anything, don't try to |
| // set a next. |
| return; |
| } |
| if (mNextPlayer != null) { |
| // Before setting up the new one clear out the old one and release |
| // it to ensure it doesn't play. |
| mPlayer.setNextMediaPlayer(null); |
| mNextPlayer.release(); |
| mNextPlayer = null; |
| mNextContent = null; |
| } |
| if (source == null) { |
| // If there's no new content we're done |
| return; |
| } |
| final MediaPlayer newPlayer = new MediaPlayer(); |
| |
| try { |
| if (headers != null) { |
| Uri sourceUri = Uri.parse(source); |
| newPlayer.setDataSource(mContext, sourceUri, headers); |
| } else { |
| newPlayer.setDataSource(source); |
| } |
| } catch (Exception e) { |
| newPlayer.release(); |
| // Don't return an error until we get to this item in playback |
| return; |
| } |
| |
| if (preparePlayer(newPlayer, false)) { |
| mPlayer.setNextMediaPlayer(newPlayer); |
| mNextPlayer = newPlayer; |
| mNextContent = new PlayerContent(source, headers); |
| } |
| } |
| |
| private void requestAudioFocus() { |
| int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, |
| AudioManager.AUDIOFOCUS_GAIN); |
| mHasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED; |
| } |
| |
| /** |
| * Start the player if possible or queue it to play when ready. If the |
| * player is in a state where it will never be ready returns false. |
| * |
| * @return true if the content was started or will be started later |
| */ |
| @Override |
| public boolean onPlay() { |
| MediaPlayer player = mPlayer; |
| if (player != null && mState == STATE_PLAYING) { |
| // already playing, just return |
| return true; |
| } |
| if (!mHasAudioFocus) { |
| requestAudioFocus(); |
| } |
| if (player != null && canPlay()) { |
| player.start(); |
| setState(STATE_PLAYING); |
| } else if (canReadyPlay()) { |
| mPlayOnReady = true; |
| } else if (!isPlaying()) { |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Pause the player if possible or set it to not play when ready. If the |
| * player is in a state where it will never be ready returns false. |
| * |
| * @return true if the content was paused or will wait to play when ready |
| * later |
| */ |
| @Override |
| public boolean onPause() { |
| MediaPlayer player = mPlayer; |
| // If the user paused us make sure we won't start playing again until |
| // asked to |
| mPlayOnReady = false; |
| if (player != null && (mState & CAN_PAUSE) != 0) { |
| player.pause(); |
| setState(STATE_PAUSED); |
| } else if (!isPaused()) { |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Seek to a given position in the media. If the seek succeeded or will be |
| * performed when loading is complete returns true. If the position is not |
| * in range or the player will never be ready returns false. |
| * |
| * @param position The position to seek to in milliseconds |
| * @return true if playback was moved or will be moved when ready |
| */ |
| @Override |
| public boolean onSeekTo(int position) { |
| MediaPlayer player = mPlayer; |
| if (player != null && (mState & CAN_SEEK) != 0) { |
| if (position < 0 || position >= getDuration()) { |
| return false; |
| } else { |
| if (mState == STATE_ENDED) { |
| player.start(); |
| player.pause(); |
| setState(STATE_PAUSED); |
| } |
| player.seekTo(position); |
| } |
| } else if ((mState & CAN_READY_SEEK) != 0) { |
| mSeekOnReady = position; |
| } else { |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Stop the player. It cannot be used again until |
| * {@link #setContent(String, boolean)} is called. |
| * |
| * @return true if stopping the player succeeded |
| */ |
| @Override |
| public boolean onStop() { |
| cleanUpPlayer(); |
| setState(STATE_STOPPED); |
| return true; |
| } |
| |
| public boolean isPlaying() { |
| return mState == STATE_PLAYING; |
| } |
| |
| public boolean isPaused() { |
| return mState == STATE_PAUSED; |
| } |
| |
| @Override |
| public long getSeekPosition() { |
| return ((mState & CAN_GET_POSITION) == 0) ? -1 : mPlayer.getCurrentPosition(); |
| } |
| |
| @Override |
| public long getDuration() { |
| return ((mState & CAN_GET_POSITION) == 0) ? -1 : mPlayer.getDuration(); |
| } |
| |
| private boolean canPlay() { |
| return ((mState & CAN_PLAY) != 0) && mHasAudioFocus; |
| } |
| |
| private boolean canReadyPlay() { |
| return (mState & CAN_PLAY) != 0 || (mState & CAN_READY_PLAY) != 0; |
| } |
| |
| /** |
| * Sends a state update if the listener exists |
| */ |
| private void setState(int state) { |
| if (state == mState) { |
| return; |
| } |
| Log.d(TAG, "Entering state " + state + " from state " + mState); |
| mState = state; |
| if (state != STATE_ERROR) { |
| // Don't notify error here, it'll get sent via onError |
| pushOnStateChanged(state); |
| } |
| } |
| |
| private boolean preparePlayer(final MediaPlayer player, boolean current) { |
| player.setOnPreparedListener(this); |
| player.setOnBufferingUpdateListener(this); |
| player.setOnCompletionListener(this); |
| player.setOnErrorListener(this); |
| try { |
| player.prepareAsync(); |
| if (current) { |
| setState(STATE_PREPARING); |
| } |
| } catch (IllegalStateException e) { |
| if (current) { |
| setError(Listener.ERROR_PREPARE_ERROR, 0, null, e); |
| } |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * @param extra |
| * @param e |
| */ |
| private void setError(int type, int extra, Bundle extras, Exception e) { |
| setState(STATE_ERROR); |
| pushOnError(type, extra, extras, e); |
| cleanUpPlayer(); |
| return; |
| } |
| |
| /** |
| * Checks if the holder is ready and either sets up a callback to wait for |
| * it or sets it directly. If |
| * |
| * @param holder |
| * @param player |
| * @return |
| */ |
| private boolean isHolderReady(final SurfaceHolder holder, final MediaPlayer player) { |
| mHolder = holder; |
| if (holder != null) { |
| if (holder.getSurface() != null && holder.getSurface().isValid()) { |
| player.setDisplay(holder); |
| return true; |
| } else { |
| Log.w(TAG, "Holder not null, waiting for it to be ready"); |
| // If the holder isn't ready yet add a callback to set the |
| // holder when it's ready. |
| SurfaceHolder.Callback cb = new SurfaceHolder.Callback() { |
| @Override |
| public void surfaceDestroyed(SurfaceHolder arg0) { |
| } |
| |
| @Override |
| public void surfaceCreated(SurfaceHolder arg0) { |
| if (player.equals(mPlayer)) { |
| player.setDisplay(arg0); |
| preparePlayer(player, true); |
| } |
| } |
| |
| @Override |
| public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) { |
| } |
| }; |
| mHolderCB = cb; |
| holder.addCallback(cb); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private void cleanUpPlayer() { |
| if (DEBUG) { |
| Log.d(TAG, mDebugId + ": Cleaning up current player"); |
| } |
| synchronized (mErrorLock) { |
| mError = null; |
| if (mErrorRetriever != null) { |
| mErrorRetriever.cancelRequestLocked(false); |
| // Don't set to null as we may need to cancel again with true if |
| // the object gets destroyed. |
| } |
| } |
| mAudioManager.abandonAudioFocus(this); |
| |
| SurfaceHolder.Callback cb = mHolderCB; |
| mHolderCB = null; |
| SurfaceHolder holder = mHolder; |
| mHolder = null; |
| if (holder != null && cb != null) { |
| holder.removeCallback(cb); |
| } |
| |
| MediaPlayer player = mPlayer; |
| mPlayer = null; |
| if (player != null) { |
| player.reset(); |
| player.release(); |
| } |
| } |
| |
| private boolean isCurrentPlayer(MediaPlayer player) { |
| return player.equals(mPlayer); |
| } |
| } |