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 
PlayerContent(String source, Map<String, String> headers)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 
AsyncErrorRetriever(long errorId)113         public AsyncErrorRetriever(long errorId) {
114             this.errorId = errorId;
115             closeHttpClient = false;
116         }
117 
cancelRequestLocked(boolean closeHttp)118         public boolean cancelRequestLocked(boolean closeHttp) {
119             closeHttpClient = closeHttp;
120             return this.cancel(false);
121         }
122 
123         @Override
doInBackground(HttpGet[] params)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 
LocalRenderer(Context context, Bundle params)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
initFeatures(Bundle params)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
onDestroy()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
onPrepared(MediaPlayer player)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
onBufferingUpdate(MediaPlayer player, int percent)281     public void onBufferingUpdate(MediaPlayer player, int percent) {
282         if (!isCurrentPlayer(player)) {
283             return;
284         }
285         pushOnBufferingUpdate(percent);
286     }
287 
288     @Override
onCompletion(MediaPlayer player)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
onError(MediaPlayer player, int what, int extra)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
onAudioFocusChange(int focusChange)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
setContent(Bundle request)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      */
setContent(Bundle request, SurfaceHolder holder)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
setNextContent(Bundle request)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 
requestAudioFocus()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
onPlay()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
onPause()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
onSeekTo(int position)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
onStop()568     public boolean onStop() {
569         cleanUpPlayer();
570         setState(STATE_STOPPED);
571         return true;
572     }
573 
isPlaying()574     public boolean isPlaying() {
575         return mState == STATE_PLAYING;
576     }
577 
isPaused()578     public boolean isPaused() {
579         return mState == STATE_PAUSED;
580     }
581 
582     @Override
getSeekPosition()583     public long getSeekPosition() {
584         return ((mState & CAN_GET_POSITION) == 0) ? -1 : mPlayer.getCurrentPosition();
585     }
586 
587     @Override
getDuration()588     public long getDuration() {
589         return ((mState & CAN_GET_POSITION) == 0) ? -1 : mPlayer.getDuration();
590     }
591 
canPlay()592     private boolean canPlay() {
593         return ((mState & CAN_PLAY) != 0) && mHasAudioFocus;
594     }
595 
canReadyPlay()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      */
setState(int state)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 
preparePlayer(final MediaPlayer player, boolean current)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      */
setError(int type, int extra, Bundle extras, Exception e)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      */
isHolderReady(final SurfaceHolder holder, final MediaPlayer player)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 
cleanUpPlayer()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 
isCurrentPlayer(MediaPlayer player)718     private boolean isCurrentPlayer(MediaPlayer player) {
719         return player.equals(mPlayer);
720     }
721 }
722