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