下面是播放的完整流程:
- 播放器加载一个网络url,首先要进行网络请求,网络如何优化,涉及到网络优化的方方面面。
- 网络拉取回来数据之后,识别一下当前视频的具体封装格式,这个可以正式流式视频,也可以是普通视频,优化的手段有点不同。
- 识别到具体的封装格式,按照封装格式的要求,开始解析封装格式,解析其中的音频流、视频流、字幕流等等。
- 音频流要解码成音频原始数据,视频流要解码成视频原始数据。
- 解码过程中注意音视频同步。
- 音频播放,同时视频开始渲染。
一、播放痛点
根据我们平时的开发实践,我们总结出播放过程中常见的几类问题:
- 播放失败率高
- 播放首帧慢
- 播放卡顿
- 播放器占用CPU、内存过高
面对这些问题,我们急切需要知道两方面的数据:
- 怎么监控这些问题
- 怎么解决这类问题
这两个问题是有有递进关系的,“怎么监控这些问题”就是为了更好地“解决这类问题”。
二、监控手段
我们得知上面的痛点,在发生这些问题时,我们要收集相应的数据分析这类问题,不然开发者一头雾水,不知道播放过程的数据信息,解决问题全靠运气。
1、网络加载监控
播放视频首要的是网络加载,网络请求是一个复杂的过程,全链路的点太多,将全链路的所有点收集起来,可以在播放器中加上网络的全链路监控:
这样我们对网络的整体加载情况有了全面的把握,发生网络加载问题,也知道是哪个点出现了问题,分析解决问题有了更加全的数据。
2、播放器全链路监控
开篇就分析了播放器的完整流程,其实开发者也非常需要当前播放器的运行状态:
播放器的工作状态也可以拆解一下:
播放器发生状态异常,开发者可以明确获知播放器当前所处的状态。
每个状态都可能发生异常,发生异常都有具体的原因。利用播放器状态、播放器出错情况构建一个较为完善的播放监控体系。
3、播放器流畅度监控
播放卡顿,就是播放过程中发生loading,UI直接显示转圈,这对用户体验的损害是巨大的,用户在不断的吐槽中默默地卸载了我们的app。卡顿的主要原因是网络状况不好,很小的一部分原因是源的问题。
- 卡顿的次数
- 卡顿的时长
- 卡顿时的网速
单次播放平均卡顿次数和卡顿时间是我们衡量播放流畅度的重要指标。
如果是源的问题,例如出现播放视频的时候,进度条在走,但是画面不走,就是视频解码出现问题,但是又没有出错,只是解码出的数据有问题。
解码出的数据有问题,有两种情况:原始数据就存在问题,这种情况下基本无法优化;另一种情况下是解码线程异常。
- MediaCodec发生异常
- 解码线程异常错误
监控发生问题时系统codec的具体状态,然后上报,便于分析问题。
三、播放成功率优化
播放失败的原因很多,使用播放器播放视频,最终都会在Player.onError回调中通知开发者播放失败了,最多返回一个错误码,对应一个播放错误。
总结而言,播放错误主要分为下面几类:
- 网络加载错误:网络请求发生问题,可能是网络请求的任何一个阶段。
- 视频格式识别错误:不支持当前的格式,或者当前格式识别出错。
- 解码出错:不支持当前视频、音频解码导致的出错,或者系统codec异常导致的问题。
- 文件的IO异常:读取缓存文件发生问题。
网络加载错误一般要视情况而定,网络超时要做好超时重试机制。
视频格式支持使用ffmpeg能解决基本上所有的视频格式的识别和处理工作。
MediaCodec解码受到手机硬件的制约,解码有时候会出错,出错可以切换到软解码。
四、播放性能优化
1、复用链接:
平时刷信息流视频的时候,其实很多视频的域名都是相同的,这些链接都是可以复用的,网络建连的时间需要30ms到200ms不等,如果能复用链接,这部分的时间是可以节省下来的。
2、预加载
一个播放器实例持有的数据非常大,player初始化的时候会初始化MediaCodec,MediaCodec对应底层的AVCodec,操作底层的/dev/codec-node,Android系统规定了系统最大持有的MediaCodec实例是16个,当然每个手机会有所不同,但总的来说不会有大的不同,就是MediaCodec的实例个数是有限的,不可能无限创建实例。
我们预加载多个播放器实例的时候,就会创建多个codec实例,超过codec实现限制,系统codec就无法正常工作,极易造成OOM或者ANR。
我们再平时解决问题的时候经常发现media.codec进程导致SystemServer卡死的。一般都是media.codec使用不当造成的。
那现在能否预加载不起播放器实例?
我们预加载的目的是为了请求视频资源,其实只需要网路模块就可以的。
本地代理可以实现将播放器的网络模块独立出来:
- 播放器不直接和视频源服务器交互,中间通过本地代理层交互。
- 本地代理层的网络加载模块是独立于播放器的,可以是播放器发起请求,也可以是其他的外部调用发起请求。
- 最终setDataSource到播放器的url是一个http://127.0.0.1:port的请求,本地代理层会通过Socket向这个url中发送数据,播放器可以直接解析数据流。跟正常的播放流程完全一样。
这样我们可以全局持有一个播放器实例:既可以做到预加载,而且可以解决播放器占用资源过多的问题。一举多得。
3、指定封装格式和解码格式:
针对一些视频,我们已经明确知道它们的封装格式和音视频的编解码格式,那我们就可以提前告知播放器这些信息,播放器直接使用特定的封装格式去嗅探,直接起特定的解码器去解码。
例如信息流视频基本上都是MP4的封装格式,H264的视频编码,AAC的音频编码。
这样我们可以节省嗅探和MediaCodec检索的时间。
4、MP4视频优化:
MP4格式的视频解析出来如下:
其中moov中包含着MP4文件特有的属性数据,mdat是具体的音频和视频数据,MP4格式规定,只有解析出moov数据之后,才能解析出mdat中具体的音频和视频数据。
但是moov有时候在mdat之前,有时间在mdat之后,如上,moov在mdat之前,那么我们顺序请求就没有问题。
但是如果moov在mdat之后,我们顺序请求就播放不出来,这时候需要起双IO缓冲加载:一个从头开始检索moov,另一个从末尾检索moov,虽找到moov,就可以先解析出moov,然后解析mdat,播放视频了。
但是双IO毕竟比较耗时,如果能在服务器提前将MP4视频的moov移到mdat之前,就可以提升MP4的首帧。
5、流式视频优化:
除了MP4视频,还有一些流式点播的视频,例如HLS格式,这些视频是一个一个的ts分片组成的。针对这些视频首帧的优化,我建议直接将前几个ts的分片的数据压缩,例如针对一个3s的ts视频,原来的分辨率是1280 * 720,现在可以压缩到320 * 180的大小,数据量大大降低,这样首帧就能快速加载下来。
6、边下边播
我们在播放视频的时候,最好能边播放边缓存到本地,这样我二次打开这个视频的时候,就可以不用请求了,直接复用本地的数据。
边下边播可以使用本地代理来完成。
7、video-id复用缓存
我们实行边下边播之后,二次打开可以复用缓存了,但是我们是根据视频的url来复用的,现在信息流视频的url经常变化的,即使是同一个视频,半小时视频url就发生了变化。
那这样的复用效率不是很低吗?
还好现在信息流都是传过来一个video-id,这个video-id不会随着视频url变化而变化,只要是同一个视频,video-id不会变化,那我们可以利用这个video-id来实现一次缓存,多次复用。
五、其他播放体验优化建议
1、播放的时候出现丢帧
播放时丢帧主要是直播应用会出现,当出现播放丢帧的情况,服务器应该主动推流低码率的流,防止客户端出现丢帧严重甚至卡住不走的情况。
2、播放画面出现锯齿
播放视频的时候出现锯齿,这时候一般是两种情况:
- 视频清晰度较高,MediaCodec对搞清的视频解码支持地较弱,画面细腻感不强。建议切换到软解码开始解码,软解码使用CPU解码,对细节的支持度很强,甚至8K的视频都能很好的解析。
- 使用GLSurfaceView替换SurfaceView或者TextureView,GLSurfaceView通过OpenGL绘制纹理实现视频细节的细腻绘制,对视频画面支持效果很好。
作者:Li Tianpeng