微信号:baiduqa

介绍:提供百度质量部的动态、技术分享等资讯.

【专家专栏】孙松:浅谈网盘视频SDK测试

2019-06-14 16:28 孙松

随着网络速度的不断提升,带宽成本的逐渐降低,信息的传递形式由文字逐渐转向语音、视频等多媒体格式,也有越来越多的业务会涉及到音视频播放,拥有一个稳定且扩展能力强的播放器内核显得尤为重要。


一、视频SDK的前世今生


为什么不使用系统的播放能力?


每个平台都会内置多媒体的播放能力,类如android平台,使用SurfaceView结合MediaPlayer即可实现音视频播放,但美中不足的是系统播放器支持的封装格式有限、视频解码性能上也较差,难以支撑复杂的业务场景。


视频SDK有哪些优势?


视频SDK能提供跨平台能力,可以运行在Android、iOS、PC三端,复用性强无需每个端重复开发、几乎支持所有常见的封装格式、SDK和业务分离,相对独立有更强的扩展性。从视觉的角度触发,下面我们直观的称视频SDK为“播放器”。


二、播放器原理介绍


一个基础的视频播放过程包含视频解码、视频渲染、音视频同步三部分


首先来解释一下为什么各大视频软件会选择使用ffmpeg这个视频开发库。gstreamer、vlc、ffmpeg都是开源的流媒体处理库,gstreamer使用gobject语言开发晦涩难懂,代码可读性较差;vlc健壮性很强但缺少相关开发文档;强大的ffmpeg代码结构设计更清晰、有更全面的开发文档,所以通常会选择ffmpeg来做视频的开发库。在开始学习之前我们应该编译好属于自己的ffmpeg,不得不说编译过程着实心酸,各种版本问题以及编译脚本的格式问题都可能会导致编译的失败,我自己在调试的时候原本想用4.1版本的ffmpeg,结果一直编译不过。下载4.0版本的源码就顺利编译通过了,由于自己调试使用也就没有深究编译不过的原因。在编译成功后会生成六个so文件,和一些头文件。在实际的应用中更倾向于将这些动态库打包成一个文件,这样对于解析崩溃会更方便一些。原理就是先将lib库打包成静态链接库,再将六个静态链接库合成一个动态库。



视频解码:


视频解码指的是从H264/H265等数据流解析成像素数据,比如解析成常见的像素数据YUV420、RGB565(像素到视频流文件的转换,压缩比例可以达到几十倍甚至上百倍)。在获取H264裸流之前我们传给ffmpeg的是一个封装后的文件。比如我们有一个文件video.flv。这种封装文件是无法进行直接解码,先要对它进行解封装。Flv的视频包含视频头信息和body体两部分(这里只针对flv视频,avi的视频并不是这种顺序存储)。Header中标示了文件类型,是否存在音频/视频流等。body体中包含Tag,视频、音频三部分。Tag中的script tag记录了音视频的相关信息,如duration、audiodatarate、width。



下图是ffmpeg对音视频进行分离的过程,创建的三个AVFormatContext(包含各种码流信息的结构体),分别用于文件输入,视频和音频文件输出avcode_copy_content()将音视频信息分别拷贝给两个结构体,然后通过区分每个packet的类型分别写入文件,就完成了从封装文件中分离音频流和视频流的过程。



在解封装之后我们获取到了视频相关信息和视频流,那么接下来ffmpeg就可以根据这些信息选择解码器,将视频流数据转换成像素数据。解码过程分为软解码和硬解码,硬解码需要在编译ffmpeg时加入硬解码相应配置并在解码器调用时做修改。硬解码借助GPU的图像处理能力减少CPU负担,降低整机功耗。但使用硬解码兼容性会降低,相对容易出现花屏,黑屏等问题。所以播放器要有软硬解切换能力。默认选择硬解,在解码失败等异常情况下自动切换为软解。


视频渲染:


Android上提供了各种view做图像展示,如SurfaceView、TextureView、GLSurfaceView。他们各具优势,在不同的场景下可以选择不同的view来进行渲染。SurfaceView 不在View hierachy的树形结构中,也就是不能像其它view一样进行操作,但在播放中选择使用SurfaceView会占用较少的内存资源。在实现GPU渲染时可以直接使用GLSurfaceView,它集成了OpengGL ES ,存在的问题是OpengGL和GLSurfaceView绑定,会被同时销毁,在使用中不够灵活,所以通常会基于HandlerThread来自己实现渲染线程和EGL环境管理,SurfaceTexture配合EGL渲染到屏幕,复杂度会变高但灵活性也得到了很大的提升。在Windows平台上由于迭代速度相对缓慢不需要较强的灵活性,可以借助libSDL库来做图像的渲染(一套代码逻辑可以在交叉编译后多平台使用)。



音视频同步:


音视频同步有三种方案,方案一参考外部时钟。视频、音频都以这个时钟为参考系。方案二以视频的时间为基准调整音频。这两种方案都会带来播放噪音的问题。通常选择方案三,以音频dts时间为基准播放视频,通常视频帧出现几帧跳动用户是无感知的,所以这种解决方案对解决音视频同步问题更加有效。



三、播放器测试


下面主要介绍在测试中遇到的痛点、踩到的坑和最后的解决方案。

第一个问题是在sdk的持续集成版本控制上:随着业务方向的扩展,迭代的增多,每个方向存在对不同版本sdk的依赖;

第二个问题是日均20+的黑屏、不能播放反馈问题如何定位;

第三个问题是针对视频加载慢、卡顿如何分析。


1.视频sdk持续集成版本控制


播放器在持续集成中采用打aar包上传到maven的方式。线下维护的maven库版本供各个迭代使用,在端上通过脚本修改版本号对插件版本进行动态的配置引用。


SDK和迭代之间存在多对多的关系,版本控制就变的让人很头大,不仅在测试中容易发生阻塞问题,错误的版本对应还可能将问题带入到线上,所以需要一个合理的方案去解决插件的版本控制问题。快照版本是提供给端开发人员和SDK的测试人员使用,release版本用于端测试和发版。这里存在的一个坑是依赖引用的版本号不做修改时,gradle仍会使用本地依赖cache进行打包。然后理所当然的想到把.gradle/cache下对应的插件缓存删除掉不就行了吗。然而这里又会出现另一个坑,SDK里的so包在端打包中莫名其妙的消失了。经过分析后得到的结论是gradle会对相同版本依赖插件下的so文件md5匹配,老的so文件被删除了,新的又匹配不上就会出现apk中缺失so的问题。那么最终有效的解决方案是快照版本在SDK打包时配置版本号自增,端测试版本“+”引用当前最新的SDK。发版版本由rd和qa同步并手工修改版本以保证发版的万无一失。



2. 黑屏及不能播放反馈问题定位


在重构视频SDK后引入了一个非常严重的体验问题,在新版本支持硬解后黑屏反馈日均约20条,主要集中在oppo、vivo、小米等厂商的机型。这种问题在线下测试中又难以复现,为了高效的找到问题的原因采用了如下的方案,通过用户反馈拉取出现问题视频的分辨率、比特率、编码方式等信息,通过以上信息对测试视频进行转码,并在线下使用相似机型自动化播放,当检测到黑屏产生时记录下测试结果,最终通过长时间的播放在某机型下找到了稳定复现黑屏的视频。在解决了这一转码bug后,全量版本反馈降低60%。在播放错误上视频SDK做了埋点统计并记录相应错误码,中控机上每小时拉取全量错误日志,出现无法播放的反馈自动去查询一遍错误日志并将错误原因记录入库,提供给客服来告知用户不能播放的原因。



3. 首帧加载慢、卡顿问题分析


日志分析最初采用单机采集日志,脚本部署在日志服务器上进行分析。经常会出现机房迁移、硬盘损坏等问题,导致数据稳定性较差,并且存在单机处理能力有限难以进行复杂数据分析的问题。为解决这一问题将数据处理迁移到spark,通过spark streaming进行实时处理。对版本、地域、运营商等维度进行数据清洗。数据清洗完成后存储到hbase,再通过离线脚本将数据转存到Mysql,卡顿问题数据量级较低可以通过SQL在业务层直接做分析。启播日志量级偏大导致查询速度偏慢,采用Mysql数据转存到ES搜索引擎的方案来提高查询效率,避免Timeout的问题。通过数据分析发现v2超会域名的加载速度慢、部分地区存在跨地域CDN请求等问题,超会首帧加载时间降低约65%。



网盘的流畅播放模式采用的是HLS协议的VOD点播,需要下载完成m3u8文件和一定数量的视频数据包后才可以开始播放,在优化首屏加载的过程中通过数据分析主要耗时在两部分:首先是网络部分耗时,其次是端业务层的耗时。在网络耗时上从两方面入手,一方面是m3u8、首个分片的压缩,通过降低文件大小提高传输速度,另一方面服务器的全链路优化,主要优化点是后端模块间的调度。端业务层耗时主要集中在数据库操作和UI绘制上,优化DB的查询机制,UI层去除对非必要元素的绘制降低端上耗时。目前发现还存在一个DNS解析耗时的优化点,部分用户出现解析时间300-500ms的情况,可以作为后续优化的方向。在提升用户体验上针对超级会员流畅播放进行边下边播、针对列表内视频做m3u8文件预取等手段来降低首屏加载时间。



在视频卡顿上主要做过两点优化,首先是视频播放支持BYTERANGE,HLS 2.0协议新增了EXT-X-BYTERANGE标签,这个特性具有分片增大,减少TCP连接数量,减少碎片文件,减少分片回源次数的优势,BYTERANGE降低卡顿率0.5%。实现方案是原先24个小分片组成一个4分钟的大分片,一次下载完毕。另一方面是CDN覆盖率优化,在日志分析过程中发现部分地区存在跨地域、运营商的请求,导致卡顿率偏高。在整理出存在问题的CDN节点后提供给CDN服务商来提高资源覆盖度从而降低卡顿率。