此文参照 android developer API Guides
https://developer.android.com/guide/topics/media/mediaplayer.html
Media Playback(媒体播放)
Android 多媒体框架能够支持多种普通媒体类型,所以我们很容易的整合音频,视频,图片到我们的应用中来。这些多媒体的资源可以是本地文件系统上的,也可以是网络上的。在android中播放音频视频使用的都是MediaPlayer apis。
这篇文档会向我们展示怎样去写一个媒体播放应用,能使得用户和系统之间有一个比较好的表现和用户的良好体验。
注意:
我们只能通过标准的输出设备来播放在音频数据,当前,指的是使用手机设备的扬声器或者蓝牙耳机。在语音通过过程中,我们不能播放声音文件。
The Basics(基础)
下面的类在Android 框架中被用来播放音视频的。
MediaPlayer:播放声音和视频的主要api。
AudioManager:用于管理音频资源以及音频在设备上的输出。
Manifest中的声明
添加两个权限:
①需要播放网络视频的需要添加INTERNET权限
②屏幕熄屏的时候,我们需要添加WAKE_LOCK权限
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WAKE_LOCK" |
如果您的播放器应用程序需要防止屏幕变暗或处理器睡觉,或使用MediaPlayer.setScreenOnWhilePlaying()orMediaPlayer.setWakeMode()方法,您必须请求许可。
Using MediaPlayer
媒体框架中的一个重要的组件是MediaPlayer类。MeidaPlayer类的对象通过简单的设置能够获取,解码和播放音视频文件。能够支持多种不同的媒体的资源:
①本地资源
②外部的URIs,比如你可以从ContentResolver中去获取。
③外部的URLs(Streaming),理解成网络上的
MediaPlayer支持的格式:
https://developer.android.com/guide/appendix/media-formats.html
demo:
http://git.oschina.net/hymKing/MediaDemo
播放本地res/raw/目录下的音频:
MediaPlayer mediaPlayer =MediaPlayer.create(context, R.raw.sound_file_1);mediaPlayer.start();// no need to call prepare(); create() does that for you
播放URIs 中的音频:
Uri myUri =....;// initialize Uri hereMediaPlayer mediaPlayer =newMediaPlayer();mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);mediaPlayer.setDataSource(getApplicationContext(), myUri);mediaPlayer.prepare();mediaPlayer.start();
播放远程网络上的音频:
String url ="http://........";// your URL hereMediaPlayer mediaPlayer =newMediaPlayer();mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);mediaPlayer.setDataSource(url);mediaPlayer.prepare();// might take long! (for buffering, etc)mediaPlayer.start();
提醒:
播放网络上的视频,可能用出现或遇到这两种异常IllegalArgumentException and IOException ,在当你使用setDataSource()方法的时候。因为有可能网络上的视频根本就不存在。
Asynchronous Preparation(异步准备)
因为视频准备过程(包括缓冲区的准备)是一个相对耗时的工作。所以异步的方式去准备视频是有必要的。所以android media框架中也提供了
prepareAsyc()用来做异步视频的准备工作。设置setOnPreparedListener方法,可以在准备完成的时候,回调onPrepared方法。也就标示了视频已经准备完成。可以去调用相应的生命周期的方法,比如start()方法去播放视频。
Managing state
(管理生命周期的状态,这部分内容在基础篇生命周期状态图中已经有说明)
google 的api guide 中再次提醒我们在编写media应用的时候,要在脑海中有media 生命周期的状态图。避免编写一些非法状态下的代码。以避免一些没有必要的异常的出现。
Releasing the MediaPlayer(释放mediaPlayer组件)
MediaPlayer能消耗Android系统比较宝贵的资源。因此我们要特别注意不要在没有必要的情况下,持有MediaPlayer实例很长时间。下面是释放资源的处理方法:
mediaPlayer.release();mediaPlayer =null;
在音乐播放器这种场景下或一些类似的场景下,即使用户已经离开了当前的activity,但是还需要播放多媒体。这时候,需要在Service中去控制MeidaPlayer。
Using a Service with MediaPlayer(使用带有多媒体的服务)
当你希望用户离开使用了MediaPlayer功能的application。或者用户切换到了别的application,但是仍然要保证应用的MediaPlayer的播放等。这就是需要使用Android提供的四大组件之一Service。让Service来控制Mediaplayer。用户和系统对运行后台服务的应用如何同系统其它部分交互有一些期望,如果你的应用无法满足这些期望,用户可能会有糟糕的体验。本节描述了你需要注意的主要事项以及如何解决的建议。
Running asychronously(异步运行)
首先,同Activity一样,默认情况下Service中的所有工作也是在主线程中完成的。因此,服务需要快速的处理传入的intent,在响应意图是不能执行长时间操作。如果有大量的工作或者阻塞式的调用,就需要异步的完成。
举个例子,当你在主线程中使用MediaPlayer,你应该使用prepareAsyc()而不使用prepare(),可以通过实现MediaPlayer.OnPreparedListener这个接口来接受准备完成的回调。这个时候,你可以启用start方法去播放多媒体。
下面是一个在service中执行的例子:
publicclassMyServiceextendsServiceimplementsMediaPlayer.OnPreparedListener{ privatestaticfinalString ACTION_PLAY ="com.example.action.PLAY"; MediaPlayer mMediaPlayer =null; publicint onStartCommand(Intent intent,int flags,int startId){ ... if(intent.getAction().equals(ACTION_PLAY)){ mMediaPlayer =...// initialize it here mMediaPlayer.setOnPreparedListener(this); mMediaPlayer.prepareAsync();// prepare async to not block main thread } } /** Called when MediaPlayer is ready */ publicvoid onPrepared(MediaPlayer player){ player.start(); }}
Handling asychronous errors (处理异步的错误)
同步操作中,错误一般会错误一般会出现在异常或者错误代码信息中。当你使用异步资源的时候,你需求确保你的应用程序在出现错误的时候,需要有错误提示。可以通过实现MediaPlayer.OnErrorListener这个接口来处理。
举例:
publicclassMyServiceextendsServiceimplementsMediaPlayer.OnErrorListener{ MediaPlayer mMediaPlayer; publicvoid initMediaPlayer(){ // ...initialize the MediaPlayer here... mMediaPlayer.setOnErrorListener(this); } @Override publicboolean onError(MediaPlayer mp,int what,int extra){ // ... react appropriately ... // The MediaPlayer has moved to the Error state, must be reset! }}
尤其要记住的是:当错误发生的时候,MediaPlayer的进入到生命周期状态图中的Error State的状态。在重新使用它之前,必须进行重新reset().
Using wake locks (使用唤醒锁)
当我们设计一个应用在后台播放媒体的时候,我们的android设备在服务运行过程中,有可能会进入到休眠状态。由于android系统为了在设备休眠状态下节省电量。系统会试图去关闭和释放相关资源。比如cpu,wifi硬件等等。但是如果我们应用程序的服务在运行着多媒体的播放。你希望防止系统熄屏对媒体播放的影响。
为了确保您的服务在这些条件下,能够继续运行,你需要添加“wake locks”,唤醒锁是一种信号系统,它发出的信号显示:应用程序正在使用或者可用的功能,后手机闲置。
注意:你应该尽量避免过多的使用唤醒锁,只有在必要的时候才去使用它。它会使设备的电池寿命大大的降低。
为了确保媒体的在播放的时候,cpu能够持续的运行。需要在初始化MeidaPlayer的时候调用setWakeMode()方法。一旦这样做了,媒体在播放的时候将持有一个指定锁 ,当媒体暂停和停止的时候会释放锁。
mMediaPlayer =newMediaPlayer();// ... other initialization here ...mMediaPlayer.setWakeMode(getApplicationContext(),PowerManager.PARTIAL_WAKE_LOCK);
然而,在这个例子中唤醒锁是指保证手机系统的cpu在唤醒状态。当你在通过网络获取媒体和您正在使用Wifi时,你可能有希望有个wifiLock,可以手动获取并释放。当你通过远程的Url准备mediaPlayer的时候,你应该创建并获得Wifi锁。代码如下:
WifiLock wifiLock =((WifiManager) getSystemService(Context.WIFI_SERVICE)) .createWifiLock(WifiManager.WIFI_MODE_FULL,"mylock"); wifiLock.acquire();
当你暂停或者停止媒体的时候,你不在需要网络的时候,需要释放这个锁:
wifiLock.release();
Running as foreground service(作为前台服务运行)
服务通常用于执行后台任务,例如获取电子邮件,同步数据,下载内容,或其他。在这些情况下,用户不会意识到这个服务的执行,甚至可能不会注意到这些服务被打断,后来重新启动。毫无疑问,后台播放音乐是一个服务,用户能意识到,任何中断都会严重影响到用户体验。此外,用户可能会希望在这个服务执行期间作用于它。这种情况,服务应该运行一个“前景服务”。前台服务在系统中持有一个更高水平的重要性,系统几乎从未将服务扼杀,因为它对用户有着直接的重要性。当应用在前台运行,该服务还必须提供一个状态栏来通知用户意识有服务正在运行同时允许他们打开一个活动,可以与服务进行交互。
为了把普通的服务转换成一个前台服务,你必须创建一个状态栏通知,然后通过service(指的就是notification,或者理解成Notification是android中实现前台服务的方式) 调用startForground()方法。例子如下
String songName;// assign the song name to songNamePendingIntent pi =PendingIntent.getActivity(getApplicationContext(),0, newIntent(getApplicationContext(),MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);Notification notification =newNotification();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);
当你的服务运行在前台的时候,你配置的通知在设备的通知区域可见的。如果用户选择了这个通知,系统会调起配置的PendingIntent。像上面的demo,系统打开了一个MainActivity。
在需要停止前台服务的场景下,调用stopForeground(true).
更多的信息,可以参见service 和Status Bar Notification的官方文档。
Handing audio focus(处理音频焦点)
在任何给定的时间,android只能有一个activity在运行,但是android系统是一个多任务的环境。这对应用程序使用音频造成了一个特别大的难度。由于只有一个音频输出,可能会有好几个媒体争夺使用它。在android2.2之前,android系统并没有一个内置的机制,来解决这个问题。所以可能导致在一些场景下,给用户一个比较坏的体验。比如用户正在听音乐,另一个应用需要通知用户非常重要的事情。由于播放的音乐用户可能听不到通知的声音。在android系统2.2之后,android系统平台微应用提供了一个方法来转让设备音频输出的使用。这个机制被称作音频焦点。
当你的应用需要输出音频的时候比如音乐通知,你应该总是要请求音频焦点。一旦有了焦点,你就能自由的使用声音,当也应该总是监听着焦点的改变。如果被通知失去了音频的焦点,需要立即杀死音频或者将当前音频的声音降低到一个较低的级别。(被称为“闪避”-有一个标识flag证明哪一个音频服务的占用),另外在重新获得焦点的时候,才恢复播放。
音频焦点能够非常自然的协作,也就是应用希望(高度建议)遵守音频焦点的指导线。但是这个规则并不是被系统强制的。如果一个应用想要播放大声音的音乐,即时在失去了焦点以后,系统也不会阻止它。然而,用户可能就得到一个会的体验,继而非常有可能会卸载掉应用。
请求音频焦点,必须调用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.}
requestAudioRequest()的第一个参数 AudioManager.OnAudioFocusChangeListener。当音频焦点改变的时候,会回调这个方法。因此你需要在你的service或者activities中去实现这个接口,举个例子:
classMyServiceextendsService implementsAudioManager.OnAudioFocusChangeListener{ // .... publicvoid onAudioFocusChange(int focusChange){ // Do something based on focus change... }}
onAudioFocusChange(int focusChange),focusChange 参数告诉我们音频焦点是如何改变的。参数的值如下(这些值都是在AudioManager中定义的)
AUDIOFOCUS_GAIN:获得焦点
AUDIOFOCUS_LOSS:你可能长时间失去了这个音频焦点,你一定停止所有的音频的播放,因为你应该不希望在后台聚焦太长时间。这是一个比较的时机或者地方去让你竟可能的释放资源。比如你需要release()
AUDIOFOCUS_LOSS_TRANSIENT:你暂时失去了音频焦点,但是应该马上就会回到焦点上。你必须停止所有的音频播放,
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:你暂时失去了音频焦点,但是你被允许继续播放音频(低音量)来替代完全的杀死音频。
下面是一个实现:
publicvoid onAudioFocusChange(int focusChange){ switch(focusChange){ caseAudioManager.AUDIOFOCUS_GAIN: // resume playback if(mMediaPlayer ==null) initMediaPlayer(); elseif(!mMediaPlayer.isPlaying()) mMediaPlayer.start(); mMediaPlayer.setVolume(1.0f,1.0f); break; caseAudioManager.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; caseAudioManager.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; caseAudioManager.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; }}
脑海中要很清晰音频焦点的apis是在api8(android2.2系统)或以上(当然现在已经几乎没有4.0系统以下的机型了,所以目前开发基本不用考虑这个问题),所以关于后续的官方文档的这部分内容就直接不用理会就可以了。
Performing cleanup(执行清理操作)
如前所说,MediaPlayer对象能够消耗大量的系统资源。所以,只是在有必要的时候让应用持有这个对象,使用完成之后要调用release()方法,进行清理。相比较于依赖于系统的垃圾回收机制,主动显示调用MediaPlayer的清理方法显得非常重要。因为系统可能花费一些时间在垃圾器回收利用MediaPlayer之前。只是因为android系统敏感的内存需求而不是媒体相关的资源的稀缺。(总之一句话,要在适当的时机去释放内存),所以当我们使用service(activity)的时候,应该覆盖onDestroy()来确保MediaPlayer被释放。
publicclassMyServiceextendsService{ MediaPlayer mMediaPlayer; // ... @Override publicvoid onDestroy(){ if(mMediaPlayer !=null) mMediaPlayer.release(); }}
你也应该尽可能的寻找一些其它的释放MediaPlayer资源的机会,而不仅仅在关闭的时候去释放。比如,如果你并不希望在某一段时间去播放Media(比如失去音频焦点之后)。你当然应该释放它然后在需要的时候,再去重新创建。另一方面,如果仅仅是暂停一下,就没有必要去频繁的释放和创建了,这样可以避免再一次的去准备。
Handling the AUDIO_BECOMING_NOISY Intent(处理音频变得吵闹的意图)
许多良好的AP会在发生一些导致音频变得混杂的时候自动停止后台播放的,例如,当用户在用耳机听歌的时候若是发生突然失去连接的情况会产生音频混杂。然而,这个行为不是自动发生的。如果你没有实现这个功能,则不会产生你需要的效果。
你可以通过处理 ACTION_AUDIO_BECOMING_NOISY 的Intent来确保你的AP停止播放音乐在那种情况下,那个Intent需要在manifest文件中注册一个receiver。
<receiverandroid:name=".MusicIntentReceiver"> <intent-filter> <actionandroid:name="android.media.AUDIO_BECOMING_NOISY"/> </intent-filter></receiver>
为这个意图注册一个广播接受器,如下:
publicclassMusicIntentReceiverextends android.content.BroadcastReceiver{ @Override publicvoid 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) } }}
接受到这个广播以后,需要通知service停止MediaPlayer。
(当然上面提到的是比较良好的应用,会做如上的处理,很显然大多应用忽视了这一块)
Retrieving Media from a Content Resolver (通过内容解释器获取媒体资源)
在媒体应用中另外一个比较有用的功能是能够获取设备上已经存在的资源。你可以通过ContentResolver来查询外部媒体。
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.}elseif(!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 =newMediaPlayer();mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);mMediaPlayer.setDataSource(getApplicationContext(), contentUri); // ...prepare and start...
搜索或扫二维码关注微信公众号Hym4Android,获取更新文章,更及时!
到此,MediaPlayer的Api guide的内容就结束了。