概述:
Android多媒体哭包含了对播放多种常见媒体的支持, 让我们可以简单的在APP中集成音视频和图形处理功能.我们可以播放APP资源中的音频或者视频文件, 也可以播放文件系统中独立的媒体文件, 或者是从网络中收取的数据流. 所有这些都是用MediaPlayer API来实现的. 下面的内容将会介绍如何实现一个媒体播放的APP, 并为用户提供高性能和良好的用户体验. 我们可以只可以将音频数据播放给标准输出设备. 当前是移动设备的话筒或者耳机. 不能在打电话的时候播放音频文件.
在Android framework中下列的两个类用来播放音视频: MediaPlayer: 该类是播放音视频的基础类. AudioManager: 用于管理音频源和音频输出.
Manifest声明:
在开始编写要使用MediaPlayer的APP之前, 需要确保在manifest中声明合适的权限. 包括:
Internet Permission – 如果使用MediaPlayer播放基于网络的码流, 那么必须要声明网络权限:
<uses-permission android:name="android.permission.INTERNET"/>
Wake Lock Permission – 如果APP需要保持屏幕不变暗或者处理器不休眠, 或者需要使用MediaPlayer.setScreenOnWhilePlaying()或者MediaPlayer.setWakeMode()方法. 则必须使用这个权限:
<uses-permission android:name="android.permission.WAKE_LOCK" />
使用MediaPlayer:
在media framework中最重要的组件就是MediaPlayer类. 该类的对象可以通过最少的设置来获取, 解码和播放音视频. 它可以支持不同的媒体源, 比如本地资源, 内部URI, 外部URL.
下面是一个使用MediaPlayer播放本地raw音频资源的例子(它保存在APP工程的res/raw/目录下):
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1); mediaPlayer.start(); // no need to call prepare(); create() does that for you
如何播放一个本地的URI:
Uri myUri = ....; // initialize Uri here MediaPlayer mediaPlayer = new MediaPlayer(); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mediaPlayer.setDataSource(getApplicationContext(), myUri); mediaPlayer.prepare(); mediaPlayer.start();
播放一个远程的HTTP URL:
String url = "http://........"; // your URL here MediaPlayer mediaPlayer = new MediaPlayer(); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mediaPlayer.setDataSource(url); mediaPlayer.prepare(); // might take long! (for buffering, etc) mediaPlayer.start();
如果提供的流的URL是一个在线的媒体文件, 该文件必须支持渐进式下载.
异步准备:
使用MediaPlayer已经很简单了, 但是有些事情还是要做的, 比如调用prepare()方法可以消耗很长的时间执行, 因为它可能要调用获取和解码媒体数据的方法. 所以, 因为这个方法可能会很慢, 我们应该总是避免在UI线程中调用该方法. 这么做会引起UI界面卡住, 体验很差. 任何超出1/10秒的操作都会引起UI响应慢. 为了避免UI线程卡顿, 需要另一个线程来处理MediaPlayer的prepare方法, 并且在结束之后提示主线程. 然而, 虽然我们可以自己写代码完成这个逻辑, 但是这部分实在是太常用了, 所以Android framework提供了更加方便的prepareAsync()方法来处理这一步骤. 该方法在后台启动prepare操作并直接返回. 当媒体prepare结束之后, MediaPlayer.OnPrepareListener中的onPrepared()方法将会被调用, 我们可以通过setOnPreparedListener()方法来设置.
管理状态:
另一个关于MediaPlayer的需要注意的是它是基于状态管理的. 就是说, 在我们写代码的时候必须注意到MediaPlayer有一个内部状态, 因为指定的操作只有在特定的状态下才能执行. 如果在错误的状态执行操作, 那么可能会引起一个异常或者其它未知的问题.
在MediaPlayer的类文档中有一个完整的状态图, 它是下面这样的, 它指明了MediaPlayer的各种状态之间的关系. 比如当我们创建一个新的MediaPlayer对象的时候, 它处于Idle状态, 这时候我们应该通过setDataSource()方法来初始化它, 初始化之后它处于Initialized状态. 这时候可以调用prepare()或者prepareAsync()方法. 当MediaPlayer准备好了之后, 它就变成了Preparing状态, 这时候我们可以用start()方法开始播放媒体. 开始播放之后我们可以使用start(), pause()和seekTo()等方法来在started, paused和playbackCompleted状态之间切换. 当我们调用stop()方法之后, 在再次prepare之前是不能调用start()的.
在写代码的时候要记得参考状态图, 在错误的状态下执行操作是常见的bug.
释放MediaPlayer:
一个MediaPlayer对象可以占用宝贵的系统资源. 因此我们应该总是采取额外的预防措施, 以保证一个MediaPlayer实例不会占用资源超出所需的时间. 当我们用完的时候应该保证调用release()方法来释放占用的系统资源. 比如, 如果我们使用MediaPlayer, 然后当activity收到了一个onStop(), 这时候必须释放MediaPlayer, 因为当activity不跟用户交互到时候还保持MediaPlayer的资源没啥意义(除非需要在后台播放). 当activity被重建或者唤醒的时候再重新创建一个新的MediaPlayer然后prepare它. 可以这样释放MediaPlayer并将其置为null:
mediaPlayer.release(); mediaPlayer = null;
为MediaPlayer指定一个Service:
如果我们想要app不在前台的时候媒体在后台播放, 就必须启动一个Service, 然后从这个Service中控制MediaPlayer实例. 我们应该细心的处理这个Service与其它组件的交互, 否则会引起不好的体验.
异步运行:
首先, 就像一个Activity, 默认情况下所有的Service的工作都是在同一个线程中工作的, 其实如果Service和activity在同一个APP中启动的话, 默认情况下它们会使用该APP的主线程. 因此Service必须快速处理传入的Intent而不能进行过多的计算. 如果有任何可能导致阻塞的繁重工作, 都应该异步执行: 或者自己启动一个额外的线程, 或者使用framework提供的多种异步处理方法. 栗如, 当在主线程使用一个MediaPlayer的时候, 我们应该调用prepareAsync()方法而不是prepare(), 然后实现一个MediaPlayer.OnPrepareListener来监听prepare是否结束, 然后就可以开始play了. 栗子:
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 = ... // initialize it here mMediaPlayer.setOnPreparedListener(this); mMediaPlayer.prepareAsync(); // prepare async to not block main thread } } /** Called when MediaPlayer is ready */ public void onPrepared(MediaPlayer player) { player.start(); } }
处理异步错误:
在异步操作的时候, 错误通常以异常或者错误码的形式被标记出来, 但是不论什么时候我们使用异步资源, 都应该确保app应该用适当的方法来处理这些错误. 在使用MediaPlayer的时候, 我们通常应该用MediaPlayer.OnErrorListener监听, 并将其设置给MediaPlayer实例:
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! } }
还有件很重要的事就是当发生一个error的时候, MediaPlayer的状态变成了Error, 在使用它之前必须要重置它.
使用唤醒锁:
当设计一款在后台播放媒体的APP时, 设备可能在Service运行的时候睡眠. 因为Android系统在睡眠的时候会尝试养护电池, 它会试着关闭所有的不需要的功能, 包括CPU和WiFi硬件. 这时候如果service在播放音乐, 我们可能希望阻止系统干扰播放. 为了确保service可以在这种情况下运行, 我们必须使用唤醒锁(wake locks). 一个唤醒锁是一种为系统提供的标记, 它表明我们的APP正在使用一些当电话空闲的时候依然保持可用的功能.
我们应该对使用唤醒锁保持谨慎态度, 只有在必要的时候才用, 因为这东西会显著降低电池的寿命.
为了确保在MediaPlayer播放的时候CPU持续运行, 在初始化MediaPlayer的时候调用setWakeMode()方法. 一旦这样做了, 当MediaPlayer开始播放的时候会加个锁并且在暂停或者停止的时候释放锁. 栗子:
mMediaPlayer = new MediaPlayer(); // ... other initialization here ... mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
但是上面代码中的锁只能保证管住CPU, 如果我们播放的是网络媒体并且在使用WiFi, 那么或许我们还要使用到WifiLock, 但是必须手动使用和释放. 所以当我们使用远程的网络资源的时候, 得创建一个WiFi锁, 栗子:
WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE)) .createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock"); wifiLock.acquire();
当暂停/停止或者不再使用网络的时候, 需要释放锁:
wifiLock.release();
作为一个前台service运行:
Service经常被用来处理后台任务, 比如获取邮件, 同步数据, 下载内容, 或者别的类似的东西. 在这些情况下, 用户不太在意service是否在运行, 甚至service被重启或中断了, 也不会注意到. 但是如果我们的service在播放音乐的话, 情况就会有点不同. 如果用户感受到任务中断了,那肯定疯了. 另外用户可能会在service运行的时候跟其它的APP交互, 这也很常见. 这种情况下, service应该作为一个前台(foreground)service运行. 一个前台service拥有更高的优先级, 系统几乎永远不会杀死一个前台service, 因为通常他们都对用户十分的重要. 当运行在前台的时候, service也必须提供一个状态栏notification来确保用户知道正在运行的服务, 并且允许用户可以打开一个可以跟service关联的activity.
为了让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). 下图是notification显示出来的的样子:
左图是notification在状态栏上的样子, 右图是它扩展开的样子.
我们应该保证在用户关心发生了什么事情的时候才显示这个状态栏, 当用户不关心的情况下这应该调用stopForeground()方法释放它.
处理音频焦点:
虽然在同一时间只有一个activity可以运行, Android依然是一个多任务环境. 这就为音频APP提出了一个挑战, 因为音频输出设备只有一个, 但是可能同时存在几个媒体service竞争使用它. Android2.2之前是没有内置的机制来解决这个问题, 所以可能在某些情况下会导致很差的用户体验. 比如, 当一个用户正在听音乐, 这时候另一个应用需要提醒用户一些比较重要的消息, 而用户可能因为音乐声音过大而无法听到这个消息的提示. 从Android2.2开始, 平台开始提供多个APP使用音频输出设备的规则, 这种机制就叫音频焦点(Audio Focus).
当我们的APP需要输出音频比如音乐或者提醒的时候, 应该总是去获取音频焦点. 一旦它拥有了焦点, 就可以自由的使用音频输出设备了, 但是应该保持监听焦点的改变. 当它失去焦点的时候, 应该直接关闭音频输出或者大幅降低输出的音量直到重新获取焦点的时候再恢复原本的音量.
音频焦点需要APP和Android合作完成, 就是说APP应当准守(也强烈建议如此)音频焦点的规则, 但是系统并不强制要求这样做. 如果一个APP在失去音频焦点的时候依然大声的播放音乐, 系统并不会阻止它. 但是这样会导致用户体验不佳, 甚至卸载该APP. 想要获取音频焦点, 必须调用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 getaudio focus.
}
requestAudioFocus()方法的第一个参数是一个AudioManager.OnAudioFocusChangeListener,它的onAudioFocusChange()方法将会在音频焦点发生变化的时候调用. 因此我们应该在自己的service或者activity中实现这个接口, 栗子:
class MyService extends Service
implements AudioManager.OnAudioFocusChangeListener{
// ....
public void onAudioFocusChange(int focusChange){
// Do something based on focus change...
}
}
focusChange参数表示音频焦点是如何变化的, 它可能是以下的某一个值(定义在AudioManager中):
AUDIOFOCUS_GAIN: 你已经获取到了音频焦点.
AUDIOFOCUS_LOSS: 表示可能长期失去了音频焦点,这种情况下应该停止所有的音频播放. 它意味着我们会失去音频焦点很久, 这时候尽量多的应该释放资源, 比如应该release 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 oftime: 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 haveto stop
// playback. We don't release the mediaplayer 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 okto keep playing
// at an attenuated level
if (mMediaPlayer.isPlaying()) mMediaPlayer.setVolume(0.1f,0.1f);
break;
}
}
要注意音频焦点的API只有在Android2.2及以上才能使用, 如果我们想要支持更早的版本, 则应该采取向后兼容的策略. 可以通过在一个指定的类中实现所有的音频焦点的功能来完成兼容(比如可以叫做AudioFocusHelper), 这里是一个栗子:
public class AudioFocusHelper implements AudioManager.OnAudioFocusChangeListener{
AudioManager mAudioManager;
// other fieldshere, you'll probably hold a reference to an interface
// that you canuse to communicate the focus changes to your Service
public AudioFocusHelper(Context ctx,/* other arguments here */){
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
// ...
}
public boolean requestFocus(){
return AudioManager.AUDIOFOCUS_REQUEST_GRANTED==
mAudioManager.requestAudioFocus(mContext,AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
}
public boolean abandonFocus(){
return AudioManager.AUDIOFOCUS_REQUEST_GRANTED==
mAudioManager.abandonAudioFocus(this);
}
@Override
public void onAudioFocusChange(int focusChange){
// let your service know about the focus change
}
}
然后在API 8及以下版本中使用它:
if (android.os.Build.VERSION.SDK_INT>= 8){
mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(),this);
} else {
mAudioFocusHelper = null;
}
执行清理:
正如前面提到的, MediaPlayer是一个很占资源的类, 所以我们应该只在需要的时候保有它, 不需要的时候就立刻释放它. 明确的调用release()方法十分重要, 而不能依赖于系统垃圾回收机制, 因为回收执行之前可能需要一点时间. 所以当我们在使用service的时候, 应该注意在onDestroy()方法中确保释放MediaPlayer:
public class MyService extends Service {
MediaPlayer mMediaPlayer;
// ...
@Override
public void onDestroy(){
if (mMediaPlayer!= null) mMediaPlayer.release();
}
}
除了关闭的时候, 还应该在其它有机会的情况下释放MediaPlayer. 比如如果长时间不能播放媒体了(失去音频焦点等), 这时候应该释放MediaPlayer然后过会儿用到的时候再创建. 如果短时间的停止则不用.
处理AUDIO_BECOMING_NOISYintent:
很多写的好的APP都会在一个事件发生导致音频变得嘈杂的时候自动停止播放. 比如原本使用头戴耳机播放音乐, 突然设备跟耳机断开了链接. 然而这种行为不会自动发生, 如果我们没有实现这个功能, 音频就会使用外放功能播放, 这或许是用户不想看到的. 我们可以通过处理ACTION_AUDIO_BECOMING_NOISY intent来解决这个问题, 只需要在manifest中这样注册一个receiver:
<receiver android:name=".MusicIntentReceiver">
<intent-filter>
<action android:name="android.media.AUDIO_BECOMING_NOISY"/>
</intent-filter>
</receiver>
这里注册了一个MusicIntentReceiver类来处理这个intent, 然后可以这样实现这个类:
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)
}
}
}
从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.
} else if (!cursor.moveToFirst()){
// no media onthe 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);
// ...prepareand start...
将相应的URI设置给MediaPlayer就OK了.
总结:
这里算是对MediaPlayer进行了一个基本的介绍, 使用方法比较简单, 除了设置MediaPlayer的固定流程之外还需要留意后台音乐播放, 以及及时释放资源.
参考: https://developer.android.com/guide/topics/media/mediaplayer.html