摘要
最近在研究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多媒体开发的还是是否有帮助的。