Android Media Playback 中的MediaPlayer的用法及注意事项(二)
Android Media Playback 中的MediaPlayer的用法及注意事项(二)
1.搭配Service来使用MediaPlayer
2.异步运行
首先,就像一个Activity一样,所有在Service的工作事实上都在一个线程里面完成的。如果你运行一个activity和service在同一个app中,那么他们默认使用的是同一个线程(即UI主线程)。因此,Serivce需要快速处理传递进来的的任务,并且不要执行大量耗时的操作后,才去响应。如果任何耗时操作或者阻塞式调用出现了,那么你就必须异步的完成这些任务。或者另开一个你自己实现的线程去解决,或者使用Android框架提供的一些异步处理的API。
例如:当你在主线程中使用MediaPlayer的时候,你应该使用prepareAsync()而不是prepare()方法,并且去实现一个MediaPlayer.OnPreparedListener以便当你的MediaPlayer的工作做完以后,你能够开始播放。
下面是一个简单的例子:
public class MyService extends Service implements MediaPlayer.OnPreparedListener { private static final String ACTION_PLAY = "com.example.action.PLAY"; MediaPlayer mMediaPlayer = null; public int onStartCommand(Intent intent, int flags, int startId) { ... if (intent.getAction().equals(ACTION_PLAY)) { mMediaPlayer = ... // 初始化MediaPlayer mMediaPlayer.setOnPreparedListener(this); mMediaPlayer.prepareAsync(); // 这样异步的准备就不会阻塞你的主线程 } } /** 当准备好以后被调用 */ public void onPrepared(MediaPlayer player) { player.start(); } }
3.处理异步的错误
在异步操作当中,错误通常是以异常或者错误码的方式被抛出,但是当你无论什么时候你使用一个异步资源,你都应该确保你的程序都会适当的得到错误发生这种通知。在MediaPlayer这个例子中,你可以实现一个MediaPlayer.OnErrorListener来做到这些。public class MyService extends Service implements MediaPlayer.OnErrorListener { MediaPlayer mMediaPlayer; public void initMediaPlayer() { // ...initialize the MediaPlayer here... mMediaPlayer.setOnErrorListener(this); } @Override public boolean onError(MediaPlayer mp, int what, int extra) { // ... react appropriately ... // The MediaPlayer has moved to the Error state, must be reset! }}
当错误发生时,MediaPlayer的状态跳变为Error State(MediaPlayer的状态图上一篇文章已经给出),记住这一点是很重要的。当错误发生时,你必须reset它然后你才可以再一次使用它。
4.使用唤醒锁
当你设计一个可以在后台播放媒体文件的app时记住,当你的后台Service还在运行的是否,你的手机可能会进入休眠的状态。在进入休眠的状态后,Android系统为了节省电量会关掉任何没有必要的功能或者后台Service等等,包括CPU,和WiFi硬件。如果你的Service正在播放或者正在通过网络播放流式文件,你肯定向阻止系统对你的Service进行这么残酷无情的操作。
为了保证你的service在这种状态下继续运行,你不得不使用唤醒锁,"wake lock",使用唤醒锁是向系统表示即使手机是空闲状态,你的程序仍然需要使用一些可用的手机的功能,例如WiFi以及CPU等
注意:你应该尽量少用唤醒锁,除非你必须使用,因为你样做会明显的减少用户手机的电量。
为了确保当你的MediaPlayer播放时,你的CPU持续运行。在你初始化你的MediaPlayer时,调用setWakeMode()方法。一旦你调用了这个,你的MediaPlayer就获得了这把锁指导你释放了它。当然你为了使用这个,你必须声明权限,上一篇文章介绍了如何声明这个权限。
mMediaPlayer = new MediaPlayer(); // ... other initialization here ... mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
但是上述的做法仅仅保证你的程序运行时,CPU是一直运行着的,如果你还正在播放着网络上的流式文件,同样你就需要一把WifiLock,这个锁你必须手动获取。
WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE)).createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock"); wifiLock.acquire();当你暂停或者停止你的MediaPlayer的时候,或者你不再需要网络的是否,你应该释放这把锁
这个同样需要声明权限
wifiLock.release();
5运行一个前台式服务
Service经常被用来执行后台任务,比如接受Email,同步数据,下载资源或者其他任务。在这些例子中,用户实际上并不太在意这些service的执行,他们也许根本不会注意到偶尔的服务被打断或者出错。
但是,考虑这么一种情形,一个Service正在播放音乐,很明显,用户是意识到这个服务的存在的,并且这个服务被突然打断,用户是立马能够察觉得到。另外,这个用户更加希望和这个播放音乐的Service进行交互,例如播放,暂停,调声音大小等。在这种情况下,这个Service应该作为一种前台式服务运行。前台式服务在系统中有较高的重要性,系统几乎重来不会去杀死他们。因为他对用户来说是很重要的。当服务运行在前台时,service必须提供一个状态栏通知确保用户注意到了这个正在运行的Service,并且运行用户通过这个状态栏通知来打开一个指定的饿activiy来和Service交互。
为了将你的Service变为前台式服务,你必须为状态栏创立一个Notification,然后在Service里面调用startForeground()
下面是一个简单的例子:
String songName;// assign the song name to songName PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0,new Intent(getApplicationContext(), MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); Notification notification = new Notification(); notification.tickerText = text; notification.icon = R.drawable.play0; notification.flags |= Notification.FLAG_ONGOING_EVENT; notification.setLatestEventInfo(getApplicationContext(), "MusicPlayerSample","Playing: " + songName, pi); startForeground(NOTIFICATION_ID, notification);当你的Service以前台式的方式运行时,你配置的notification在手机的通知区域是可见的,如果用户点击了这个notification,那么系统将调用你指定的PendingIntent,在上面的例子中,将打开一个activity(MainActivity)
下面是一副示意图
你应该保持你的Service是foreground service状态仅仅是你的Service确实是在执行用户察觉得到的活动。一旦完成了任务,你应该通过调用stopForeground()方法来释放它。
stopForeground(true);
6.处理音频焦点(audio focus)
即使在一个时间仅仅有一个activity能够运行,但是android仍然是一个多任务的运行环境。这就给使用音频的app提出了实际的难题,因为手机只有一个输出设备,多个media service应该竞争它来使用。在Android2.2之前,没有内在的机制来定位这个问题的发生,这在某些情况下会导致坏的用户体验。例如,当用户正在听音乐的时候,另一个app有重要的事情需要通知给用户,这种情况下由于播放音乐的声音很大可能用户就同不见通知了。但是从Android2.2开始,平台为app提供了可以互相协商使用audio的方式----这个方式被称为Audio Focus(音频焦点)
当你的app需要输出一个音频例如音乐、通知,你应该始终去请求audio focus。一旦app获取到了焦点,app就可以自由使用声音输出了,但是app应该去一直监听焦点的变化。如果app被通知它已经失去audio focus那么它应该立刻要么结束audio的播放或者将音量调小到安静的水平(这个现象被称为ducking,就是声音快速的变小),再一次获取到焦点时,才继续大声的播放
audio focus事实上是互相合作完成的。这就是说,app应该遵守这个audio focus的规则,但是系统并不会强app去执行这个规则。如果一个app想在失去audio focus后仍然大声的播放音乐,系统将会不做出任何阻止。但是,用户将获得一个坏的体验,很有可能就把这个不守规矩的app给卸载掉了。
为了获得audio focus,你必须调用AudioManager的requestAudioFocus()方法,下面是一个简单的例子:
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { // could not get audio focus. }
requestAudioFocus()的第一个参数是一个实现了AudioManager.OnAudioFocusChangeListener的对象,无论什么时候有一个audio focus的变化,这个接口的onAudioFocusChange()都会被回调,因此你应该在你的activity或者service实现这个。
class MyService extends Service implements AudioManager.OnAudioFocusChangeListener { // .... public void onAudioFocusChange(int focusChange) { // Do something based on focus change... } }这个参数 focusChange 告诉你audio focus是怎么样的改变,会是下面的常量值之一,这些常量值在AudioManager中被定义
- AUDIOFOCUS_GAIN: 你已经获得了audio focus
- AUDIOFOCUS_LOSS:你已经失去了audio focus很长一段时间。你必须停止播放你,因为你很长一段时间你都不会获得audio focus,这个时候你就应该清理你占用的资源。比如说你释放掉MediaPlayer
- AUDIOFOCUS_LOSS_TRANSIENT: 你暂时性的失去了焦点,不过很快就会再一次获得焦点。你必须停止播放,但是不用释放资源因为你很快就会回来播放他们。
- AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:你暂时失去了焦点,但是允许你在一个音量小的水平下继续播放,而不是完全结束你的播放
下面是一个简单的例子:
public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: // resume playback if (mMediaPlayer == null) initMediaPlayer(); else if (!mMediaPlayer.isPlaying()) mMediaPlayer.start(); mMediaPlayer.setVolume(1.0f, 1.0f); break; case AudioManager.AUDIOFOCUS_LOSS: // Lost focus for an unbounded amount of time: stop playback and release media player if (mMediaPlayer.isPlaying()) mMediaPlayer.stop(); mMediaPlayer.release(); mMediaPlayer = null; break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: // Lost focus for a short time, but we have to stop // playback. We don't release the media player because playback // is likely to resume if (mMediaPlayer.isPlaying()) mMediaPlayer.pause(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: // Lost focus for a short time, but it's ok to keep playing // at an attenuated level if (mMediaPlayer.isPlaying()) mMediaPlayer.setVolume(0.1f, 0.1f); break; } }
7.执行清理操作
正如前面一篇文章中提到的,MediaPlayer对象是非常消耗资源的,因此如果你做完你的工作后,你应该调用release()去释放它。
显示调用这个方法是非常重要的,而不是依赖系统的垃圾回收器去回收,因为gc只是在内存紧张的时候才会去回收,而不是在与MediaPlayer相关的资源紧张的时候去回收。
8.处理 AUDIO_BECOMING_NOISY Intent
当用户拔下耳机的是否,一些优秀的App会自动停止播放音乐。(例如QQ音乐在你拔下耳机的时候自动暂停)。但是,这种功能并不是制自动的,而是需要你自己去实现。如果你不实现这个,可能会导致坏的用户体验。比如你的用户带着耳机在教室或者图书馆使用你的app播放多媒体文件,不小心拔掉了耳机或者耳机插头松动,那么后果就是导致非常差的用户体验。
在这种情况下,你可以通过在你的manisfest文件中注册一个receiver,或者用其他的方式注册也可以。
<receiver android:name=".MusicIntentReceiver"> <intent-filter> <action android:name="android.media.AUDIO_BECOMING_NOISY" /> </intent-filter></receiver
然后实现你的这个receiver的类
public class MusicIntentReceiver extends android.content.BroadcastReceiver { @Override public void onReceive(Context ctx, Intent intent) { if (intent.getAction().equals(android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { // signal your service to stop playback // (via an Intent, for instance) } } }
9.从Content Resolver中搜索多媒体
在多媒体app中另一个有用的功能就是检索用户手机上的所有的音乐,你可以实现这个功能通过查询Content Resolver
ContentResolver contentResolver = getContentResolver(); Uri uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; Cursor cursor = contentResolver.query(uri, null, null, null, null); if (cursor == null) { // query failed, handle error.} else if (!cursor.moveToFirst()) { // no media on the device} else { int titleColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media.TITLE); int idColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media._ID); do { long thisId = cursor.getLong(idColumn); String thisTitle = cursor.getString(titleColumn); / / ...process entry... } while (cursor.moveToNext()); }
为了使用这个MediaPlayer,你可以这么写:
long id = /* retrieve it from somewhere */; Uri contentUri = ContentUris.withAppendedId( android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id); mMediaPlayer = new MediaPlayer(); mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mMediaPlayer.setDataSource(getApplicationContext(), contentUri); // ...prepare and start...
总结:本次文章还是主要讲MediaPlayer,只是引入了Service组件和它一起使用。
主要讲了:
1.异步的去做一些MediaPalyer初始化的工作,要么自己单独开线程或者实现一个接口来使用mediaPlayer提供的API prepareAsync()
2.实现一个错误发生的监听接口,并在对应的回调方法中处理这些错误
3.防止手机休眠导致的CPU停转和网络WiFi等关闭,申请权限并在程序实现中获取相应的锁
4.当你的Service是用户可以察觉到的或者有用户非常关心的,那么应该将Service作为一个前台式服务,foreground service,并且实现你的Notification能够跳转到你指定的Activity或者其他组件
5.音频焦点的问题,为了避免你的MediaPlayer遮盖了其他的声音输出,你应该在你播放期间时刻监听音频焦点的变化从而随时做出播放的调整(或者是暂停,调小音量等)
6.当用户摘下耳机或者其他任何导致用户输出变得嘈杂的时候,你应该去接受这个由系统发出的广播,从而做出处理
7.使用Content Resolver实现用户手机上可用的Media文件的扫描