公司做投屏项目的,需要播放各种客户端推送的直播点播视频,根据做播放器的一些经验,这里总结一些在应用层使用播放器的注意事项和一些优化方法。
PS:个人经验之谈,不具备权威性
播放器的使用过程中常能见到以下的几种问题:
- 黑屏有声音
- 播放失败
- 播控状态保护
- ANR
- 倍速播放的问题
一、黑屏有声音
黑屏有声音是一种见的比较多的问题,能导致这种问题的原因很多。大致有以下几类:
- 播放器调用异常
- 播放器抢占
- 播放器底层创建或解码异常
- 播放器能力不足
- 对Android标准api的支持不友好
1、播放器调用异常
对Android播放器的使用要小心翼翼,调用要小心小心再小心,一旦有一丢丢的疏忽,立马就会黑脸给你看。
下面的播放器调用问题会导致黑屏:
1) surface有异常
surface对播放器来说是一个很重要的东西,不管在应用层你是用SurfaceView、GLSurfaceView还是TextureView,最终都要生成一个surface给到播放器。如果这个surface有异常,就会导致播放器解码的数据无法正确渲染,可能会出现黑屏。
在应用层我们要保证这个surface是可用的并且是干净的,也就是说我们需要做到以下几点:
- 需要在surface完全创建好之后才能调用播放器的setSurface方法
- 在surface被销毁后,要停止播放器
- surface不复用,播放一个视频,创建一个新的surface
2)视频源不可用
当视频源不可用的时候,一般的设备上,播放器会回调onError,但是在部分设备上会黑屏,不会回调onError。碰到这种设备的时候,尽量不要使用系统播放器,能避开系统播放器就避开。在这种设备可以考虑使用第三方播放器,ijk、vlc等。如果避不开,也要做一些处理,假如视频源是本地视频,可以先检查文件存不存在,size是否为0,若这个视频是从服务器下载的,可以校验md5是否正确;假如这个视频源是在线视频,那么需要添加一个加载超时检测机制,比如说30秒内如果还未加载到视频,就可以认为是播放失败。
3) start和seek
要在合适的时候调用start,在onPrepare、onSeekComplete、onInfo的MEDIA_INFO_BUFFERING_END消息中及时的调用MediaPlayer的start方法,部分设备上会因为在onPrepare中没有调用mediaPlayer.start导致视频黑屏有声音。
在播放直播视频的时候,在部分设备上调用MediaPlayer的seek也会出现黑屏,那么在seek之前要检查当前视频是否是直播视频,当前是根据视频的时长来判断是否为直播的,一般情况下直播的视频时长是0,在部分设备上播放器可能会返回-1,除此之外,还有一些直播平台的视频是2秒,这些在做直播视频判断的时候都要考虑到。
4)SurfaceView设置 setZOrderMediaOverlay(true)
在海信电视上如果使用SurfaceView可以设置此属性
2、播放器抢占
一般的一个Android设备只有一个硬件解码器,若在解码器使用期间再次创建一个新的解码器就会出现异常,导致视频播放黑屏。所以在创建播放器的时候要保证当前没有正在使用的播放器实例。若当前正在播放视频,此时用户从手机上投屏了一个新的视频,那么需要等待前一个新的播放器完全释放之后才能创建新的播放器。这就是播放器的抢占问题,程序自身内的播放器抢占可以等待前一个视频播放器完全释放之后再创建新的播发器,如果抢占的是第三方app的播放器,那就需要有一种方式通知第三方关闭播放器,这种就需要app开发者与第三方app沟通好,提出一种双方认可的方式来完成这个操作。
3、播放器底层创建或解码异常
播放器出现异常不可怕,可怕的是出现异常还不给应用层回调onError,下面两段log分别是创建播放器异常和播放器解码异常,不回调onError消息,然后就黑屏了,上层代码完全无感知
Video: h264 (avc1 / 0x31637661), yuv420p, 720x1272, 1749 kb/s
mime=video/avc, profile=100, level=31
MS_OMX_H264DEC: Error Code: 3001007 0 0 0
.......
11913 10-11 17:58:28.639 2322 9297 D SurfaceUtils: set up nativeWindow 0xd1956008 for 1280x720, color 0x32315659, rotation 0, usage 0x40002930
11914 10-11 17:58:28.640 2322 9297 E OMXNodeInstance: setParameter(0xd18f8bc0:MS.AVC.Decoder, ParamPortDefinition(0x2000001)) ERROR: BadParameter(0x80001005)
11915 10-11 17:58:28.640 2322 9297 W ACodec : [OMX.MS.AVC.Decoder] setting nBufferCountActual to 6 failed: -22
11916 10-11 17:58:28.640 2322 9297 E OMXNodeInstance: setParameter(0xd18f8bc0:MS.AVC.Decoder, ParamPortDefinition(0x2000001)) ERROR: BadParameter(0x80001005)
11917 10-11 17:58:28.640 2322 9297 W ACodec : [OMX.MS.AVC.Decoder] setting nBufferCountActual to 5 failed: -22
碰到这样的情况,有资源的可以将这种情况反馈到设备厂商或芯片厂商帮忙排查问题,同时在应用层需要针对这些设备机型或视频源做一些特殊处理,可以尝试切换SurfaceView、GLSurfaceView、TextureView,同时可以考虑使用第三方播放器。
4、播放器能力不足
4k视频、h265视频、mpeg4视频,对播放器本身有要求,若芯片不支持解码这些格式,也会出现播放黑屏的现象。对于h265视频、mpeg4视频可以尝试使用第三方播放器的软解播放,对于4k视频,给用户弹个toast提示吧,不支持是真没办法。
5、对Android标准api的支持不友好
部分设备上设置循环播放导致黑屏,通常情况下是设置循环播放属性之后,第二轮播放的时候会黑屏,可能是因为用这个属性的app不多,部分厂商的播放器不太关注这里。在这些设备上就要把使用循环播放的功能给禁掉,这种配置可以在程序里面根据机型做个判断,也可以在后台服务器下发,这两种方式最好结合一起使用。
二、播放失败
导致播放失败的原因有很多,有些是视频源异常,有些是播放器异常。在播放器异常的时候,我们就可以尝试使用第三方播放器再播放一次,通常情况下是可以播放成功的。比如说有些设备不支持flv格式的视频播放,切换到ijk播放器就可以播放成功。
在播放器的选择上,默认情况下,我们会优先选择系统播放器,我们相信厂商自身会比我们更在意他们自己的播放器能力,而且有能力的厂商一般都有专门的播放器团队,出现问题也能通过他们自身的资源去找芯片厂商处理。
关于第三方播放器,基于自身的见识,推荐ijk或vlc,我们项目是使用ijk作为备用播放器,从上线几年的情况来看,还是挺好的。国内有一个大的电视机厂商播放器系统底层是使用vlc来做的,从我个人的体验来看,也是很好的。
还有一类播放失败是由于在做了不合法的播放器操作导致的,比如在prepared状态下调用了pause,有些设备上的播放器可能回调onError,这种情况下我们要做好播控状态保护机制。
三、播控状态保护
从上图可以看出,播放器的播控操作对当前的播放状态是有要求的,比如上面提到的在prepared状态下调用了pause,有些设备上的播放器可能回调onError,从上图可以看出pause在播放器started之后才能调用。关于如何实现你自己的播控状态保护,可以参考Android源码VideoView中的播控状态保护。
public class VideoView extends SurfaceView
implements MediaPlayerControl, SubtitleController.Anchor {
private static final String TAG = "VideoView";
// all possible internal states
private static final int STATE_ERROR = -1;
private static final int STATE_IDLE = 0;
private static final int STATE_PREPARING = 1;
private static final int STATE_PREPARED = 2;
private static final int STATE_PLAYING = 3;
private static final int STATE_PAUSED = 4;
private static final int STATE_PLAYBACK_COMPLETED = 5;
...
// mCurrentState is a VideoView object's current state.
// mTargetState is the state that a method caller intends to reach.
// For instance, regardless the VideoView object's current state,
// calling pause() intends to bring the object to a target state
// of STATE_PAUSED.
private int mCurrentState = STATE_IDLE;
private int mTargetState = STATE_IDLE;
...
@Override
public void start() {
if (isInPlaybackState()) {
mMediaPlayer.start();
mCurrentState = STATE_PLAYING;
}
mTargetState = STATE_PLAYING;
}
@Override
public void pause() {
if (isInPlaybackState()) {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
mCurrentState = STATE_PAUSED;
}
}
mTargetState = STATE_PAUSED;
}
private boolean isInPlaybackState() {
return (mMediaPlayer != null &&
mCurrentState != STATE_ERROR &&
mCurrentState != STATE_IDLE &&
mCurrentState != STATE_PREPARING);
}
}
VideoView的mCurrentState记录当前播放器的状态,在做pause和start操作的时候,会先检查播放器的状态。
添加播控状态保护,一定程度上也可以减少播放器抛出IllegalStateException、减少播放器ANR
/**
* Pauses playback. Call start() to resume.
*
* @throws IllegalStateException if the internal player engine has not been
* initialized.
*/
public void pause() throws IllegalStateException {
stayAwake(false);
_pause();
}
private native void _pause() throws IllegalStateException;
另外这里提一下,在调用MediaPlayer这些方法的时候,尽量添加try{}catch(){},怎么着也不能崩溃,是不?
四、ANR
如果你的播控状态保护做好了,仍然发生anr,那么请联系厂商解决,一般情况下不建议自行处理。比如在有些设备上,播放器在onPrepare回调之前调用release,一定会出现anr,开始我们做了处理,若果当前视频未加载成功,只调用stop,不调用release,这个anr问题确实解决了。但是在后来的测试抢占逻辑的时候出现了很多视频黑屏,厂商反馈说是由于前一个视频未调用release导致的,最后我们将代码回退,厂商处理的那个问题。如果联系不到厂商,只能自行处理,也请处理之后仔细全面的做一次播放器的全用例测试,避免引起更严重的问题。
五、倍速播放的问题
现在国内水剧太多,很多人都用倍速播放看剧,这里简单提下,在调用Android标准倍速播放接口的时候,会发现大部分的TV、盒子设备的标准接口是无效的,甚至调用会导致崩溃,那么在TV、盒子上实现倍速播放建议使用第三方播放器来做。
标准播放器接口的无效的情况还有其它的,例如setDataSource(String path, Map<String, String> headers)
,在很多设备上也无效,所以在TV、盒子上使用系统播放器还要仔细的测试设备的兼容性。