一.音视频播放工具
1.1 MediaPlayer
我们平时最常使用MediaPlayer类来进行音视频的播放,其实它完成的是一个解码并播放的过程,并且是一个高度封装的工具类。使用它播放视频的时候我们还要配合SurfaceView或者TextureView来进行,后面的两个UI控件完成画面的展示。后来在前面的基础之上,系统提供了一个封装好的VideoView,为我们带来更简洁的调用。
任何事情都是有利弊两端的,简便使用的同时带来了可扩展性的问题。MediaPlayer的调用位于java层,内部的很多具体实现虽然也位于java层但是并不对应用开发者开放。那么就会带来一些问题,例如:视频容器文件格式的支持问题;缓冲区大小的定制;下载进度等。为了解决上面的问题,Media Codec API组应运而生。
1.2Media Codec API组
MediaPlayer把Extractor,和Codec API全部封锁在了Framework层,应用层完全接触不到。在新的API设计里面,这些api都对开发者开放了,我们可以定制自己的播放器了。
在这里面,最重要的就是MediaExtractor和MediaCodec这两个类,第一个功能是对容器文件进行读取控制,第二个是对数据进行编解码。
1.3Android目前支持的视频,音频,图片格式和通信协议
Android目前支持的视频,音频,图片格式和通信协议;这是官方的说明
二.MediaPlayer相关
2.1MediaPlayer状态切换
MediaPlayer的生命周期状态切换十分重要,对于正确使用具有指导意义
。下面是状态切换图:
主要的状态
- Idle 状态
- End 状态
- Error 状态
- Initialized状态
- Prepared状态
- Preparing状态
- Started状态
- Paused状态
- Stopped状态
- PlaybackCompleted状态
状态切换,切换流程
- 椭圆代表MediaPlayer驻留的状态
- 带箭头直线代表播放控制且驱动MediaPlayer状态进行切换过渡。 有两种类型的带箭头直线,单箭头表示的是同步调用,双箭头代表异步调用
Idle 状态
Idle:清闲,备用状态。
进入Idle状态可以通过两种途径,一种是直接new MediaPlayer()创建一个实例,便进入Idle状态;另一种在使用完之后调用reset()方法,进入Idle状态;
进入Idle状态不同途径的不同
。
- new MediaPlayer()的方式:Idle状态下,调用如下stop(), seekTo(),getCurrentPosition(), getDuration(), getVideoHeight(), getVideoWidth(), setVolume(float, float), pause(), start(), prepare(), prepareAsync()等方法(此处不完全列出),即使事先注册了setOnErrorListener,也不会受到onError的回调,同时MediaPlayer实例的状态也不会发生变化。
- reSet()方式:Idle状态下,调用如下stop(), seekTo(),getCurrentPosition(), getDuration(), getVideoHeight(), getVideoWidth(), setVolume(float, float), pause(), start(), prepare(), prepareAsync()等方法(此处不完全列出),如果事先注册了setOnErrorListener,那么就会回调onError方法。
推荐:Idle状态之后不要做例如上面的错误操作,如果是reSet之后到达Idle状态,最好随手调用release方法,释放掉资源使其到达End状态。
注意 new MediaPlayer()和MediaPlayer.create()构造实例的不同
。
使用new MediaPlayer()会进入Idle状态,而MediaPlayer.create()由源码可知调用prepare()进入了Prepared状态。
End状态
当在Idle状态中此时调用release
方法,此时进入End状态;release方法作用是将MediaPlayer的实例的所有占用的资源完全释放,End状态下MediaPlayer的资源完全释放,处于完全结束状态。及时释放所占用资源很重要,建议在不在使用的时候及时释放。
Error状态
造成Error状态的情况比较多,在实际开发中如果不熟悉MediaPlayer的状态流程,使用的场景又较复杂,在开发中很可能出现,例如不支持的文件格式,视频的分辨率问题等等。所以事先setOnErrorListener注册错误监听器很有必要,可以帮助我们找到错误原因。
如果想逃出Error状态, 可以使用 reset() 方法进入 Idle 状态
;
Initalized状态
在 Idle 状态调用 setDataSource() 方法, MediaPlayer 到 Initialized 状态;注意只能在Idle状态下调用setDataSource() 进入,如果其他状态下调用则会抛出异常。
Prepared状态和Preparing状态
Initialized 状态时候调用 prepareAsync() 方法(异步方式),或者prepare()方法(同步方式)会进入Prepared状态。
注意:1.只有Initialized 状态时候通过上面两种方式才能进入Prepared状态,其他状态下想通过上面两个方法中一种进入Prepared状态都会报异常
。2.使用prepareAsync() 方法(异步方式)的时候会进入一个短暂的Preparing状态
,事先注册好setOnCompletionListener,进入Prepared状态的时候会有回调。3.Prepared状态下可以设置音视频的属性(音量setVolume等),播放循环模式(setLooping()),常亮setScreenOnWhilePlaying等。
Started (开始) 状态
在 Prepared 状态调用 start() 方法, MediaPlayer 即到了 Started 状态;
判断 MediaPlayer 是否在 Started 状态 : 在任何状态下调用 isPlaying() 方法, 可以判断 MediaPlayer 是否在 Started 状态;Started状态下,可以在事先注册好的setOnBufferingUpdateListener中监听onBufferingUpdate得到相应的缓存进度。Started状态下,再调用start方法不会起总用。
Paused状态
Started 状态调用 pause() 方法, MediaPlayer 会进入 Paused 状态;Paused 状态调用 start() 方法, 恢复 Started 状态。
Started 状态转换为 Paused状态是瞬间的
; 在 Paused 状态调用 start() 方法, 会进入 Started 状态,恢复之前的播放进度,这个过程是异步的,需要一段时间
;当处于Paused状态的MediaPlayer重复调用pause方法是无效的。
Stoped状态
在 Prepared, Started, Paused, PlaybackCompleted 状态下 调用 stop() 方法, MediaPlayer 都会变为Stopped 状态;此状态下不同于Paused状态可以通过start方法恢复为Started状态,只能通过prepare方法或者prepareAsync方法回到Prepared状态,再通过start方法回到Started状态。处于Stoped状态时候重复调用stop方法无效。
PlaybackCompleted状态
如果播放时候开启了重复播放模式setLooping(true)
,Started状态在音频或者视频播放完成之后将重新进行播放。
如果没有 开启重复播放模式(默认),Started状态在播放完成之后会变为PlaybackCompleted模式。我们可以在事先注册的setOnCompletionListener()中的onCompletion中收到回调,我们也可以下这里直接调用start方法来恢复到Started状态。
播放进度调整seekTo()
该方法可以在 Prepared, Started,Paused, PlaybackCompleted 状态进行调用;
我们可以事先注册监听器setOnSeekCompleteListener,在onSeekComplete中获取回调;可以通过getCurrentPosition()获取播放进度。
2.2MediaPlayer的使用
/**
* Created by gxo on 2019/2/25.
*/
public class RadioPlayer implements MediaPlayer.OnPreparedListener
,MediaPlayer.OnCompletionListener,MediaPlayer.OnErrorListener{
private MediaPlayer mediaPlayer;
private String playUrl;
//暂停
public void pause(){
mediaPlayer.pause();
}
//判断是否在播放
public boolean isPlaying(){
return mediaPlayer.isPlaying();
}
//停止
public void stop(){
mediaPlayer.stop();
}
//播放一个网络音频
public void play(String url){
playUrl = url;
initMediaplayer();
}
private void initMediaplayer() {
mediaPlayer = new MediaPlayer();
//准备完成监听
mediaPlayer.setOnPreparedListener(this);
//播放完成监听
mediaPlayer.setOnCompletionListener(this);
//播放错误监听
mediaPlayer.setOnErrorListener(this);
try {
mediaPlayer.setDataSource(playUrl);
mediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onPrepared(MediaPlayer mp) {
Mlog.e("onPrepared");
mediaPlayer.start();
}
@Override
public void onCompletion(MediaPlayer mp) {
Mlog.e("onCompletion");
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
Mlog.e("onError");
return false;
}
}
上面是一个为了展示api的demo,很简单就能实现音频的播放,视频的播放当然要增加展示View如SurfaceView或者TextureView,这里不做展示,百度一下很多。
同时在新的api中可以看出,自带的MediaPlayer添加了字幕和保护版权的加密处理
。
三.Media Codec API组
实现的功能
其实主要是MediaExtractor和MediaCodec两个工具类,前者的主要功能是加载控制,后者是编解码功能。使用这个api组合就是要自己完成数据的加载和解码成可以在Android系统中展示的视频和播放的音频。
实际开发中遇到的问题
投入实际生产中,个人建议还是使用FFmpeg实现会好很多。在开发过程中会遇到各种问题,例如视频和音频同步问题,尤其是在播放网络流的时候,FFmpeg相对来说各个方面的资料较多,可以帮助我们完成开发。
这里不贴出具体的使用代码,百度一下写的不错的也很多,例如https://juejin.im/post/5aa6527e6fb9a028c06a79e3和https://www.jianshu.com/p/ec5fd369c518。