/* * 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 headers; public PlayerContent(String source, Map headers) { this.source = source; this.headers = headers; } } private class AsyncErrorRetriever extends AsyncTask { 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 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 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); } }