摘要
最近在研究Android多媒体开发,于是就看了下官方关于多媒体的一些开发指南,本文对指南的主要内容做一些记录,以便实际开发时参考。参考文章:
MediaPlayer介绍
状态机
音视频文件(或流)的播放控制是通过下图所示的状态机来管理的,图例说明:
- 椭圆代表的是播放器可能达到的状态;
- 弧线代表的是驱动状态改变的控制操作;- 单箭头的弧线代表的是同步调用;
- 双箭头的弧线代表的是异步调用;
 

关于播放器状态图的几点说明:
- 生命周期(Idle开始,End结束),当采用new关键字创建播放器或者调用播放器的reset方法后,它将处于Idle状态,当播放器调用release()方法后,它将处于End状态。从Idle到End这个过程,就是播放器的生命周期。- Idle状态下(不论是new创建的还是调用reset得到的)调用播放器操作(如play、pause等)被认为是编码错误,这种情况(即一新建对象就调用prepare等方法)。但两种情况有些许区别,通过new达到的Idle状态时用户提供的OnErrorListener.onError()不会被播放器内核调用,并且播放器状态不会改变;但通过reset达到Idle时OnErrorListener.onError()会被调用,并且播放器状态且切换至Error状态;
- 当不再使用播放器是,强烈建议调用release方法以释放播放器对象关联的播放内核引擎资源。原因是这些资源会持有硬件加速组件的单例引用。当调用release失败时会让播放器回滚至软件加速实现(或者两者都失败),这样就进入到了End状态,而一旦播放器进入End状态,就不能再改变其状态了。
- 补充一点,使用new得到的播放器处于Idle状态,但使用重载的create系列方法得到的播放器却处于prepared状态(前提是创建成功了)。
 
- Idle状态下(不论是new创建的还是调用reset得到的)调用播放器操作(如
- 导致播放操作调用失败的原因有很多,如播放格式不支持、分辨率不支持(太高)、播放流响应时间太长等,因此错误报告处理和播放器状态恢复就十分重要。但是上面说了,错误的调用时有发生(如上面提到的情况),为了保证这种情况我们能获取到回到通知,我们可以通过setOnErrorListener相关提前注册错误监听器,这样即使是由于我们在错误的状态调用博昂起操作,也能得到错误通知。- 通常情况下,即使我们没有提供错误监听器,播放器一旦错误就会进入Error状态(当然上面提到的那种情形除外);
- 为了复用对象,一旦错误,我们可以调用reset方法来进行恢复;
- 在应用中提前注册错误监听是个好习惯;
- 为防止编码错误,在错误的状态调用prepare、prepareAsync、setDataSource等方法,会抛出IllegalStateException;
 
- Idle状态下调用setDataSource系列方法成功后会将播放器状态从Idle改变成Initialized;
- 在播放之前,播放器必须先到到Prepared状态,达到后如果有注册OnPreparedLister,其onPrepared()方法会被调用,有两种途径:- 通过调用prepare方法和通过调用prepareAsync方法(先到Preparing状态,再到Prepared状态)
- Preparing是一个过渡状态
- 非Initialize和Stopped状态下调用prepare和prepareAsync会抛出IllegalStateException异常;
- Prepared状态下,可以通过调用相应方法来调整音量、保持屏幕常亮、循环控制等;
 
- 通过调用
- 成功调用start方法后进入Started状态,可以通过isPlaying()来判断是否处于Started状态- Started状态下可以通过设置的OnBufferingUpdateListener.onBufferingUpdate来跟踪缓存的状态;
- 播放器已经处于Started状态是再调用start方法对播放状态没有影响;
 
- Started状态下可以通过设置的
- 播放时可以暂停和停止,同时可以调整播放的位置(分别对应pause、stop、seekTo三个方法),pause成功调用后,便进入了Paused状态,从Started状态到Paused状态的过程是异步的,所以状态改变时立即调用isPlaying来判断可能不准确。- 从Paused状态恢复到播放状态是,其位置是暂停时的位置,start调用成功后,播放器将恢复至Started状态;
- Paused状态下再调用pause对播放器状态无影响;
 
- 从Paused状态恢复到播放状态是,其位置是暂停时的位置,
- 调用stop()可以让播放器从Started、Paused、Prepared和PlaybackCompleted状态切换至Stopped状态;- Stopped状态下只有调用prepare和prepareAsync成功后进入Prepared状态才可以再次进入Started状态;
- Stopped状态下再调用stop无影响;
 
- Stopped状态下只有调用
- 可以通过seekTo来调整播放位置:- 虽然异步调用seekTo能很快的返回,但实际的seek操作会花费一定时间,尤其是在线的音视频流情况下。当seek真正完成时,注册的OnSeekComplete.onSeekComplete()会被调用;
- Prepared、Paused和PlaybackCompleted状态下也可以调用seekTo方法;
- 可以通过getCurrentPosition来获取当前播放的位置;
 
- 虽然异步调用
- 当播放到音视频流的结尾时,播放就完成了,此时:- 如果是循环模式,播放器就仍处于Started状态;
- 如果是非循环模式,播放器就调用注册的OnCompletion.onCompletion()回调,并进入PlaybackCompleted状态;
- 播放完成后调用start可以从头播放
 
合法与非法的状态
MediaPlayer的成员方法大多都有属于其合法的调用状态和非法的调用状态,这个可以通过上面的状态机来查看,也可以查看官方提供的表格。如下:
| Method Name | Valid States | Invalid States | Comments | 
|---|---|---|---|
| attachAuxEffect | {Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} | {Idle, Error} | This method must be called after setDataSource. Calling it does not change the object state. | 
| getAudioSessionId | any | {} | This method can be called in any state and calling it does not change the object state. | 
| getCurrentPosition | {Idle, Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} | {Error} | Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state. | 
| getDuration | {Prepared, Started, Paused, Stopped, PlaybackCompleted} | {Idle, Initialized, Error} | Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state. | 
| getVideoHeight | {Idle, Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} | {Error} | Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state. | 
| getVideoWidth | {Idle, Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} | {Error} | Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state. | 
| isPlaying | {Idle, Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} | {Error} | Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state. | 
| pause | {Started, Paused, PlaybackCompleted} | {Idle, Initialized, Prepared, Stopped, Error} | Successful invoke of this method in a valid state transfers the object to the Paused state. Calling this method in an invalid state transfers the object to the Error state. | 
| prepare | {Initialized, Stopped} | {Idle, Prepared, Started, Paused, PlaybackCompleted, Error} | Successful invoke of this method in a valid state transfers the object to the Prepared state. Calling this method in an invalid state throws an IllegalStateException. | 
| prepareAsync | {Initialized, Stopped} | {Idle, Prepared, Started, Paused, PlaybackCompleted, Error} | Successful invoke of this method in a valid state transfers the object to the Preparing state. Calling this method in an invalid state throws an IllegalStateException. | 
| release | any | {} | After release(), the object is no longer available. | 
| reset | {Idle, Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, Error} | {} | After reset(), the object is like being just created. | 
| seekTo | {Prepared, Started, Paused, PlaybackCompleted} | {Idle, Initialized, Stopped, Error} | Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state. | 
| setAudioAttributes | {Idle, Initialized, Stopped, Prepared, Started, Paused, PlaybackCompleted} | {Error} | Successful invoke of this method does not change the state. In order for the target audio attributes type to become effective, this method must be called before prepare() or prepareAsync(). | 
| setAudioSessionId | {Idle} | {Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, Error} | This method must be called in idle state as the audio session ID must be known before calling setDataSource. Calling it does not change the object state. | 
| setAudioStreamType (deprecated) | {Idle, Initialized, Stopped, Prepared, Started, Paused, PlaybackCompleted} | {Error} | Successful invoke of this method does not change the state. In order for the target audio stream type to become effective, this method must be called before prepare() or prepareAsync(). | 
| setAuxEffectSendLevel | any | {} | Calling this method does not change the object state. | 
| setDataSource | {Idle} | {Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, Error} | Successful invoke of this method in a valid state transfers the object to the Initialized state. Calling this method in an invalid state throws an IllegalStateException. | 
| setDisplay | any | {} | This method can be called in any state and calling it does not change the object state. | 
| setSurface | any | {} | This method can be called in any state and calling it does not change the object state. | 
| setVideoScalingMode | {Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} | {Idle, Error} | Successful invoke of this method does not change the state. | 
| setLooping | {Idle, Initialized, Stopped, Prepared, Started, Paused, PlaybackCompleted} | {Error} | Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state. | 
| isLooping | any | {} | This method can be called in any state and calling it does not change the object state. | 
| setOnBufferingUpdateListener | any | {} | This method can be called in any state and calling it does not change the object state. | 
| setOnCompletionListener | any | {} | This method can be called in any state and calling it does not change the object state. | 
| setOnErrorListener | any | {} | This method can be called in any state and calling it does not change the object state. | 
| setOnPreparedListener | any | {} | This method can be called in any state and calling it does not change the object state. | 
| setOnSeekCompleteListener | any | {} | This method can be called in any state and calling it does not change the object state. | 
| setPlaybackParams | {Initialized, Prepared, Started, Paused, PlaybackCompleted, Error} | {Idle, Stopped} | This method will change state in some cases, depending on when it’s called. | 
| setScreenOnWhilePlaying | any | {} | This method can be called in any state and calling it does not change the object state. | 
| setVolume | {Idle, Initialized, Stopped, Prepared, Started, Paused, PlaybackCompleted} | {Error} | Successful invoke of this method does not change the state. | 
| setWakeMode | any | {} | This method can be called in any state and calling it does not change the object state. | 
| start | {Prepared, Started, Paused, PlaybackCompleted} | {Idle, Initialized, Stopped, Error} | Successful invoke of this method in a valid state transfers the object to the Started state. Calling this method in an invalid state transfers the object to the Error state. | 
| stop | {Prepared, Started, Stopped, Paused, PlaybackCompleted} | {Idle, Initialized, Error} | Successful invoke of this method in a valid state transfers the object to the Stopped state. Calling this method in an invalid state transfers the object to the Error state. | 
| getTrackInfo | {Prepared, Started, Stopped, Paused, PlaybackCompleted} | {Idle, Initialized, Error} | Successful invoke of this method does not change the state. | 
| addTimedTextSource | {Prepared, Started, Stopped, Paused, PlaybackCompleted} | {Idle, Initialized, Error} | Successful invoke of this method does not change the state. | 
| selectTrack | {Prepared, Started, Stopped, Paused, PlaybackCompleted} | {Idle, Initialized, Error} | Successful invoke of this method does not change the state. | 
| deselectTrack | {Prepared, Started, Stopped, Paused, PlaybackCompleted} | {Idle, Initialized, Error} | Successful invoke of this method does not change the state. | 
权限相关
媒体播放需要的权限根据其实现的功能主要有以下三个:
- 播放网络媒体需要INTERNET权限;
- 播放时保持屏幕常亮需要WAKE_LOCK权限;
- 实现播放缓存功能可能需要读写SD卡权限;
回调处理
MediaPlayer提供了比较多的回调监听处理,具体有:
- setOnPreparedListener,播放器准备完毕;
- setOnVideoSizeChangedListener,播放器尺寸改变;
- setOnSeekCompleteListener,拖动完成;
- setOnCompletionListener,播放完成;
- setOnBufferingUpdateListener,缓存更新;
- setOnInfoListener,有可用信息或警告时;
- setOnErrorListener,发生错误时;
MediaPlayer需要在拥有自己Looper的线程中创建。
开发指南
Android的多媒体框架支持播放大多数媒体类型,可以很方便的在应用中整合多媒体播放功能。
基础部分
Android多媒体框架有两个主要的类:MediaPlayer和AudioManager,前者主要负责音视频播放的基础API,后者主要管理设备上的音频资源和音频输出。
权限相关
上面已经提到了,主要需要以下几种权限:
- 网络权限,播放网络资源时需要;
- WAKE_LOCK权限,保持屏幕常亮时需要(为了防止电量消耗,在不需要是请记得取消屏幕常亮的设置)
- 存储权限,缓存功能需要
MediaPlayer的使用
可以用来播放以下三种类型:
- Local Resource,即raw目录下的资源;
- 本机资源,即通过ContentResolver获取到的手机上存在的音视频资源;
- 网络资源,即网络音视频流;
播放raw资源
| 1 | MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1); | 
播放本机资源
| 1 | Uri myUri = ....; // 获取本机资源的Uri | 
播放网络资源
| 1 | String url = "http://........"; // 网络资源地址 | 
注意:播放的网络资源必须是可以逐步下载的。同时要捕获IllegalArgumentException和IOException因为请求的网络资源可能是不存在的。
异步资源准备
原则上可以直接使用MediaPlayer进行媒体播放,但是考虑到资源的准备可能花费较长时间,如果直接在UI线程调用,会阻塞UI,导致ANR的产生。所以我们在子线程中进行资源的准备,在准备好后发出通知即可。虽然我们可以自己进行相关的线程切换管理,但是MediaPlayer已经提供了已由的Api给我们调用,我们可以通过prepareAsync()来准备,然后再OnPreparedListener的onPrepared()方法中进行处理。OnPreparedListener可以通过setOnPreparedListener来设置。
状态管理
MediaPlayer是基于状态类的,所以在编码时要十分注意状态管理,通常情况下状态是按如下节奏变化的:
Idle->Initialized(->Preparing)->Prepared->Started(->Paused->PlaybackCompleted)->Stopped,等,状态的变化都是有相应的方法驱动的。
资源释放
由于MediaPlayer及其消耗系统资源,在不再使用它是请及时进行资源的释放。
| 1 | mediaPlayer.release(); | 
在Service中使用MediaPlayer
如果需要支持后台播放的功能,那么就要使用Service进行MediaPlayer的播放操作。官方建议使用MediaBrowserServiceCompat和MediaBrowserCompat来实现,它是一种C/S架构。通常是一个持有MediaPlayer的Service实例来进行播放的。
异步执行
即使是后台播放,因为Service通常和Activity一样默认运行在主线程的,所以Service中同样也不能进行耗时操作。所以Service播放时同样要采用异步,并在准备完成后通知播放器进行播放。通常实现如下:
| 1 | public class MyService extends Service implements MediaPlayer.OnPreparedListener { | 
异步过程中的错误处理
同步的播放操作在发生错误或异常后马上就会得到通知,但异步的情形是就需要我们自己在错误回调里面进行处理了,在Service里处理异步错误的代码如下:
| 1 | public class MyService extends Service implements MediaPlayer.OnErrorListener { | 
WakeLock处理
WakeLock说明
后台播放之所以需要请求Wakelock,是因为当设备休眠是,系统为了省电,通常会关闭哪些不必要的服务,如CPU、WiFi,然而我们播放的时候是不希望这种情况发生的,所以我们就要自己处理下WakeLock,MediaPlayer处理WakeLock很简单,代码如下:
| 1 | mediaPlayer = new MediaPlayer(); | 
播放网络资源时,要防止WiFi被关,请求WiFi的WakeLock代码如下:
| 1 | // 请求WiFi 的WakeLock | 
注意我们在不必要的时候就要及时释放WakeLock,毕竟WakeLock会减少设备电池的寿命。
资源清理
前面已经提到了,MediaPlayer会消耗大量系统资源,所以我们在不再使用时要及时进行资源的释放,在后台播放的情形下,通常用如下的方式来进行资源的清理:
| 1 | public class MyService extends Service { | 
DRM管理
即Digital Right Management,数字版权管理,Android 8.0(API 26)提供了支持播放拥有数字版本的多媒体文件的Api,它与MediaDrm相似,但更抽象,且只实现了MediaDrm的部分功能。下面的代码片段展示了同步使用DRM MediaPlayer的一般过程:
| 1 | setDataSource(); | 
异步初始化DRM信息
通过注册OnDrmInfoListener和OnDrmPreparedListener来获取异步调用完毕后的回调,然后再相应的回调中进行后续操作。
| 1 | setOnPreparedListener(); | 
加密资源处理
Android 8.0(API 26)MediaPlayer开始部分支持解密Common Encryption Scheme(CENC)、H.264(METHOD=SAMPLE-AES)和AAC,之前就支持Full-segment encrypted media (METHOD=AES-128)。
支持不完善,一般不采用。
总结
本文主要介绍了Android官方播放器MediaPlayer的一般使用方法及使用的一些注意事项,还对一些常见的播放场景做了说明,对刚接触android多媒体开发的还是是否有帮助的。