Android 音视频基础——官方关于多媒体的说明介绍

https://rainmonth.github.io/posts/A191208.html

摘要

最近在研究Android多媒体开发,于是就看了下官方关于多媒体的一些开发指南,本文对指南的主要内容做一些记录,以便实际开发时参考。参考文章:

MediaPlayer介绍

状态机

音视频文件(或流)的播放控制是通过下图所示的状态机来管理的,图例说明:

  • 椭圆代表的是播放器可能达到的状态;
  • 弧线代表的是驱动状态改变的控制操作;
    • 单箭头的弧线代表的是同步调用;
    • 双箭头的弧线代表的是异步调用;

MediaPlayer状态图

关于播放器状态图的几点说明:

  • 生命周期(Idle开始,End结束),当采用new关键字创建播放器或者调用播放器的reset方法后,它将处于Idle状态,当播放器调用release()方法后,它将处于End状态。从Idle到End这个过程,就是播放器的生命周期。
    • Idle状态下(不论是new创建的还是调用reset得到的)调用播放器操作(如playpause等)被认为是编码错误,这种情况(即一新建对象就调用prepare等方法)。但两种情况有些许区别,通过new达到的Idle状态时用户提供的OnErrorListener.onError()不会被播放器内核调用,并且播放器状态不会改变;但通过reset达到Idle时OnErrorListener.onError()会被调用,并且播放器状态且切换至Error状态;
    • 当不再使用播放器是,强烈建议调用release方法以释放播放器对象关联的播放内核引擎资源。原因是这些资源会持有硬件加速组件的单例引用。当调用release失败时会让播放器回滚至软件加速实现(或者两者都失败),这样就进入到了End状态,而一旦播放器进入End状态,就不能再改变其状态了。
    • 补充一点,使用new得到的播放器处于Idle状态,但使用重载的create系列方法得到的播放器却处于prepared状态(前提是创建成功了)。
  • 导致播放操作调用失败的原因有很多,如播放格式不支持、分辨率不支持(太高)、播放流响应时间太长等,因此错误报告处理播放器状态恢复就十分重要。但是上面说了,错误的调用时有发生(如上面提到的情况),为了保证这种情况我们能获取到回到通知,我们可以通过setOnErrorListener相关提前注册错误监听器,这样即使是由于我们在错误的状态调用博昂起操作,也能得到错误通知。
    • 通常情况下,即使我们没有提供错误监听器,播放器一旦错误就会进入Error状态(当然上面提到的那种情形除外);
    • 为了复用对象,一旦错误,我们可以调用reset方法来进行恢复;
    • 在应用中提前注册错误监听是个好习惯;
    • 为防止编码错误,在错误的状态调用prepareprepareAsyncsetDataSource等方法,会抛出IllegalStateException;
  • Idle状态下调用setDataSource系列方法成功后会将播放器状态从Idle改变成Initialized;
  • 在播放之前,播放器必须先到到Prepared状态,达到后如果有注册OnPreparedLister,其onPrepared()方法会被调用,有两种途径:
    • 通过调用prepare方法和通过调用prepareAsync方法(先到Preparing状态,再到Prepared状态)
    • Preparing是一个过渡状态
    • 非Initialize和Stopped状态下调用prepareprepareAsync会抛出IllegalStateException异常;
    • Prepared状态下,可以通过调用相应方法来调整音量、保持屏幕常亮、循环控制等;
  • 成功调用start方法后进入Started状态,可以通过isPlaying()来判断是否处于Started状态
    • Started状态下可以通过设置的OnBufferingUpdateListener.onBufferingUpdate来跟踪缓存的状态;
    • 播放器已经处于Started状态是再调用start方法对播放状态没有影响;
  • 播放时可以暂停和停止,同时可以调整播放的位置(分别对应pausestopseekTo三个方法),pause成功调用后,便进入了Paused状态,从Started状态到Paused状态的过程是异步的,所以状态改变时立即调用isPlaying来判断可能不准确
    • 从Paused状态恢复到播放状态是,其位置是暂停时的位置,start调用成功后,播放器将恢复至Started状态;
    • Paused状态下再调用pause对播放器状态无影响;
  • 调用stop()可以让播放器从Started、Paused、Prepared和PlaybackCompleted状态切换至Stopped状态;
    • Stopped状态下只有调用prepareprepareAsync成功后进入Prepared状态才可以再次进入Started状态;
    • Stopped状态下再调用stop无影响;
  • 可以通过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
2
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); // 上面讲了,通过create得到的MediaPlayer实例无需prepare

播放本机资源

1
2
3
4
5
6
Uri myUri = ....; // 获取本机资源的Uri
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();

播放网络资源

1
2
3
4
5
6
String url = "http://........"; // 网络资源地址
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare(); // 这里由于缓冲可能需要的时间比较久
mediaPlayer.start();

注意:播放的网络资源必须是可以逐步下载的。同时要捕获IllegalArgumentException和IOException因为请求的网络资源可能是不存在的。

异步资源准备

原则上可以直接使用MediaPlayer进行媒体播放,但是考虑到资源的准备可能花费较长时间,如果直接在UI线程调用,会阻塞UI,导致ANR的产生。所以我们在子线程中进行资源的准备,在准备好后发出通知即可。虽然我们可以自己进行相关的线程切换管理,但是MediaPlayer已经提供了已由的Api给我们调用,我们可以通过prepareAsync()来准备,然后再OnPreparedListener的onPrepared()方法中进行处理。OnPreparedListener可以通过setOnPreparedListener来设置。

状态管理

MediaPlayer是基于状态类的,所以在编码时要十分注意状态管理,通常情况下状态是按如下节奏变化的:
Idle->Initialized(->Preparing)->Prepared->Started(->Paused->PlaybackCompleted)->Stopped,等,状态的变化都是有相应的方法驱动的。

资源释放

由于MediaPlayer及其消耗系统资源,在不再使用它是请及时进行资源的释放。

1
2
mediaPlayer.release();
mediaPlayer = null;

在Service中使用MediaPlayer

如果需要支持后台播放的功能,那么就要使用Service进行MediaPlayer的播放操作。官方建议使用MediaBrowserServiceCompat和MediaBrowserCompat来实现,它是一种C/S架构。通常是一个持有MediaPlayer的Service实例来进行播放的。

异步执行

即使是后台播放,因为Service通常和Activity一样默认运行在主线程的,所以Service中同样也不能进行耗时操作。所以Service播放时同样要采用异步,并在准备完成后通知播放器进行播放。通常实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyService extends Service implements MediaPlayer.OnPreparedListener {
private static final String ACTION_PLAY = "com.example.action.PLAY";
MediaPlayer mediaPlayer = null;

public int onStartCommand(Intent intent, int flags, int startId) {
...
if (intent.getAction().equals(ACTION_PLAY)) {
mediaPlayer = ... // 初始化
mediaPlayer.setOnPreparedListener(this);
mediaPlayer.prepareAsync(); // 异步prepare
}
}

/** 准备好后开始播放 */
public void onPrepared(MediaPlayer player) {
player.start();
}
}

异步过程中的错误处理

同步的播放操作在发生错误或异常后马上就会得到通知,但异步的情形是就需要我们自己在错误回调里面进行处理了,在Service里处理异步错误的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyService extends Service implements MediaPlayer.OnErrorListener {
MediaPlayer mediaPlayer;

public void initMediaPlayer() {
// 初始化媒体播放器
// 设置错误监听
mediaPlayer.setOnErrorListener(this);
}

@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// 错误后的处理
// 播放器已经进入错误的状态,需要重置后才能继续使用
}
}

WakeLock处理

WakeLock说明
后台播放之所以需要请求Wakelock,是因为当设备休眠是,系统为了省电,通常会关闭哪些不必要的服务,如CPU、WiFi,然而我们播放的时候是不希望这种情况发生的,所以我们就要自己处理下WakeLock,MediaPlayer处理WakeLock很简单,代码如下:

1
2
3
mediaPlayer = new MediaPlayer();
// ... other initialization here ...
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);

播放网络资源时,要防止WiFi被关,请求WiFi的WakeLock代码如下:

1
2
3
4
5
6
7
8
// 请求WiFi 的WakeLock
WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");

wifiLock.acquire();

// 在合适的时机释放WakeLock
wifiLock.release();

注意我们在不必要的时候就要及时释放WakeLock,毕竟WakeLock会减少设备电池的寿命。

资源清理

前面已经提到了,MediaPlayer会消耗大量系统资源,所以我们在不再使用时要及时进行资源的释放,在后台播放的情形下,通常用如下的方式来进行资源的清理:

1
2
3
4
5
6
7
8
9
10
public class MyService extends Service {
MediaPlayer mediaPlayer;
// ...

@Override
public void onDestroy() {
super.onDestroy();
if (mediaPlayer != null) mediaPlayer.release();
}
}

DRM管理

即Digital Right Management,数字版权管理,Android 8.0(API 26)提供了支持播放拥有数字版本的多媒体文件的Api,它与MediaDrm相似,但更抽象,且只实现了MediaDrm的部分功能。下面的代码片段展示了同步使用DRM MediaPlayer的一般过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setDataSource();
setOnDrmConfigHelper(); // 可选项,用于个性化配置
prepare(); // 资源准备(实际使用时这里是异步调用的)
if (getDrmInfo() != null) { // 获取DRM信息,如果不为空就处理
prepareDrm(); // 准备drm信息(这个过程通常需要异步处理,避免阻塞,通过OnDrmPreparedListener获取回调通知)
getKeyRequest(); // 获取不透明的密钥请求字节数组以发送到许可证服务器
provideKeyResponse(); // 将获取到的密钥响应通知到DRM引擎
}

// MediaPlayer is now ready to use
start();
// ...play/pause/resume...
stop();
releaseDrm();

异步初始化DRM信息

通过注册OnDrmInfoListener和OnDrmPreparedListener来获取异步调用完毕后的回调,然后再相应的回调中进行后续操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
setOnPreparedListener();
setOnDrmInfoListener();
setDataSource();
prepareAsync();
// ...

// 如果是受版权保护的内容,在这个回调方法里面进行处理
onDrmInfo() {
prepareDrm();
getKeyRequest();
provideKeyResponse();
}

// 当prepareAsync()结束后,将收到onPrepared()回调通知,如果是一个DRM内容,onDrmInfo()会优先于onPrepared()被调用,所以这里可以开始播放了
onPrepared() {

start();
}

加密资源处理

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多媒体开发的还是是否有帮助的。