移动端视频缓存保障与CDN调度优化

2019-08-16 14:38:12 浏览数 (1)

本文由网易云信资深音视频客户端工程师张根宁在LiveVideoStackCon 2019上海音视频技术大会的演讲整理而成,张根宁分享了团队在线视频播放器优化的主要方向,即缓冲和卡顿问题。对于卡顿,可以优化CDN转发过程来解决缓存不足的问题,通过无缝切换保证流畅度。针对首屏秒开,可以通过合适的切流措施和多CDN的灾备策略来保证拉流成功率,而优化的根本在于首屏流程中移除耗时操作。

文 / 张根宁

整理 / LiveVideoStack

我是来自网易云信的张根宁,今天我将会站在用户的角度来跟大家探讨播放器的相关优化,也会详细阐述网易云信团队在播放器方面都做了哪些努力。

1. 移动端播放器优化实践

我本人也是一个短视频的爱好者,在看头条抖音的时候根本停不下来,一刷就是几个小时就过去了。我时常就在想什么原因能把我粘在这里,后来我想明白了,除内容因素之外有两点是最吸引我的:第一所见即所得。点击的同时可以把我想要的内容呈现。第二,在播放过程当中不会给我有任何停顿感。

1.1 播放器

从这个反向推,我觉得在播放器的播放过程当中给用户最不好的体验就是这两点,一个是开始的频繁缓冲,第二个是在播放的过程当中的卡顿。这两点在播放器里会涉及到两个关键的指标:卡顿率和秒开率。今天的内容主要是将如何针对这两点来进行优化。

我们的播放器底层基于ijkplayer,跟大部分的播放器的底层架构一样,有三个外部视频输入,(像网络视频)经过输入之后会经过解协议,到下面解封装,音视频流分离进行解码、同步、最后呈现。

关键点在两块缓存,第一个缓存是原始数据缓存,第二个解码出来的数据缓存。今天的所有的优化会针对这两个缓存进行。

把播放器从拿到流到播放流理解成一个工厂,有三种原因会导致流播不出来:

1.根本没拿到,所谓的缓存不足,存货不足。2.拿到了,但是性能有限处理不过来,只能频繁丢帧。给用户的感觉要么就是慢速播放,要么频繁跳帧。3.时间戳是乱的。这种是比较极端的情况,可能是上行的推流端在错误的情况下或者在CDN转发的时候出现了错误,这种卡顿只能及时检测。

1.2 为什么缓存里没有数据了?

其实从整个链路来看,无论是点播还是直播都是大概经过这三个方向:

比如直播。上行会推个流到CDN。点播采用CDN回源去点播原站去拿视频流,最终终端会从CDN的一个节点上拉取视频流。

但在这三个节点上面每一个都有可能会发生卡顿:

第一,在推流端发出来的时候视频流丢了。(针对于这种情况我们推流端做了很多工作,像QS实时侦测网络带宽、根据网络带宽调整分辨率以及码率进行频繁发送)

第二,在CDN转发过程丢帧或者不及时。这种是主要优化对象。因为播放质量跟CDN是息息相关的,如果CDN不及时本地再怎么优化也于事无补。第三,本地带宽不够。当用户在本地网络不好的时候这是最常见的一种卡顿。从节点上看,在回来的时候(当前的解包能力和解析流的能力是不会占用太多的CPU和内存的)主要的性能瓶颈就是在解码。与渲染相比,一些低端的IOS(像4,4S解一个1080P视频)解码器的性能不够,它在30毫秒之内吐不出来一帧,所以在用户看到的时候已经变成慢速播放。而有些帧可能在送到解码器之前就已经丢掉了。或者渲染能力不行,解码能力虽然跟上了,但无法在指定的时间内渲染出来图像给用户。

1.3 卡顿优化-CDN质量

我们服务端有调度服务器来控制CDN的选择。

服务端选择CDN的流程。播放器发起调度请求的时候,调度服务器会对所有的CDN进行一个可用NGB调度:把所有的CDN节点都返回进行简单测速,以及根据统一上报的信息来进行权重,把这些信息返回再做相应处理。

而在客户端会进行二次测速。因为服务端是基于大数据来筛选出来CDN质量评优,而在本地还要参照当地网络带宽情况再根据服务端给的推荐权重比例,会有下面的公式:Pn是服务端给的权重,成正比。在拿到每个CDN结点的时候跟每条CDN依次建连,时间消耗是Tn。Tn越小这条流的质量越高。

如果经过一系列的处理,发现在CDN质量还是不高,此时应该分两步来走。

第一步,感知卡顿。依赖一套完备的卡顿处理监控体系。在典型模型里面这三块对应三块缓冲区,会有数据流入和数据流出量,以及当前数据平缓度的统计。渲染会有丢帧率的统计,根据统计上报信息在服务端计算当前的网络状态以及数据情况,给出最优的当前卡顿率结果。就可以判断当前的卡顿率范围属于正常还是异常。

具体的指标有网络、ISP、位置作为维度参与运算。卡顿统计精确到秒级。示范卡顿上报数据,以每秒打一个点来标记当前正在处于卡顿,以一分钟一个周期进行上报。在服务端拿到数据的时候就可以看到第五秒到第十秒已经发生了卡顿,其他属于正常情况。卡顿时长、卡顿频率都可以通过数据解析出来,比较精细化。

第二步,感知卡顿统计,快速恢复播放分为两方面:

1. 服务端CDN不行了。通过两轮校验,没有办法提供正常的流,服务端经过统计服务器报警之后会自动将CDN下发权重降低,而当前CDN会列为CDN的黑名单节点,这样后上来的用户就不会再拉到这个CDN给他的节点去拉流。

2.客户端会进行灾备地址的切换。切换的维度有三个:第一个要保证当前Ping公网对本地网络进行有效的评估,利用Ping公网包的方式来判断当前带宽是否处于正常的状态。第二个看缓冲池,手机缓冲数据流入量的平稳度。第三,计算当前卡顿总时长。当数据流入很平稳并且本地带宽很充裕的情况下,单位时间内卡顿时长上来了,就认为是CDN没有及时给到东西,会根据当前的综合历史记录进行切流。

1.4 本地性能优化

首先应该要去除网络的因素。要保证Buffer的数据流入量是正常的。保证本地流入量(本地的带宽)正常把网络因素去除。第二,看渲染丢帧率。当渲染没办法以正常比例帧率渲染,会在缓存Buffer里面均匀丢帧来保证渲染流畅度。统计的丢帧率越大渲染性能越差,这表示当前渲染的性能出了问题。

去掉最左边和最右边的因素之后再去评价解码器的流入跟流出数据的平缓度,就能确定当前是解码还是渲染引出的问题。

这个问题可以分两个方面。

渲染性能问题。第一,IOS9以后会引入一些高效渲染方式(如Metal),这种渲染方式天生就比OpenGL要高效。第二,有畸形的黑白名单,根据畸形来判断渲染的最佳比例(Max渲染的Size),根据渲染图像等比例裁减后保证当前的渲染图像流畅播放。第三,引入丢帧机制。在性能极差,损失画面弥补不了的情况下会现均匀丢帧。牺牲用户产生跳帧,来保证流畅播放。

解码性能不足怎么办?解码性分为软解码和硬解码。

1.因为软解码依赖FFmpeg解码,软解码引入多线程解码

2.优先推荐硬解码来代替解码(比如低端的机型在解H265的时候CPU会飙到一百到一百五左右,推荐使用使用硬解码代替软解码)。

3.采取隔帧参考的方式,根据推流端协商,在某一帧进行跳帧的参考。在这种编码方式下播放器可以在解码前把一些不必要的非参考帧丢弃,还原出原来的图像。

1.5 点播卡顿

关于点播。点播会采用无缝切换用牺牲画质来获取流畅度:两条视频流在切换的过程当中时间上保持连续性,用户不知不觉降低了画质而且画面不间断。

大概的实现流程:视频流A在HD的T1点卡顿会开启T2点B低分辨率的码流。当T1点播放完成(T1点红色部分就是T1当前所缓存的所有数据)第一批数据播放完成之前如果T2已经完成了加载,直接切到T2。没有完成加载也切到T2,因为可以判定当前的低分辨率网络也不行,会按照这个顺序继续降级。(当然这种降级可以供用户在选择是自动还是手动)。

这种无缝切换有种问题:当切换点刚好处于两个I、一个GoP中间,而这个GoP是比较大,可能会产生画面回跳。

因为播放器是生产者消费者的模式,消费掉的数据会丢弃。解码解到这里前面的帧已经不可见了,Seek到这一点服务端强制从第一个I帧开始解,用户的直观体验就是画面回跳。针对这种的优化是把当前的一整个GoP全部都保存下来,只针对前面这一部分进行解码,不输出,就不会渲染在画面上,解到这一点再继续输出。这样做在用户体验上可以达到画面连续的效果。

经过一系列从推流端到服务端到终端的优化,之前的用户负荷(百万级日活量)在接入之后整体卡顿率平均能下降30 %

1.6 秒开

其实要先看一下,播放器在秒开之前都做了什么。

正常的流程播放器流程大概是这样。因为有CDN节点,第一个步骤是跟调度服务器进行HTTP交互。第二步在拿到节点之后Socket建连的过程。第三步从流解析到解码最后渲染。在优化之前做了测试,网络极佳性能没有问题的情况下,调度服务器在200到500毫秒之间,Socket也是在200 ,后续也都是在100到200毫秒,总体加起来再打开会在一千毫秒以上,达不到秒开的效果。

1.7 GSLB调度优化-预调度

针对以上问题对每个节点进行了优化 以第一个节点为例,GSLB调度必须在播放的时候同步等HTTP返回,拿到最优节点才能进行播放。引入方案:把结果提前存起来,用的时候从缓存里拿。

引入模型:有一个队列,用户可以提前加入一批他拿到的URL地址,交给调度分为两块缓存来存,一块是4G,一块是WiFi。播放的时候会判断缓存里有没有。没有,开始调度。有的话,直接缓存命中去播。这样可以节省首屏的HTTP请求的时间。

在这个方向还会有两个问题。第一,调度速度。第二,基于调度优先级。

因此在考虑并发性的情况下,允许最大的并发数量为4 - 6个。为了保证优先级遵循先进先出,先进先调的顺序。而有即时的调度任务会优先调度,并且在避免带宽抢占的情况下会把预调度里面的所有队列里面的任务暂停,优先调度。

另外CDN调度,本身即时有效,比如说用户在某一时间、某一网络状态、在某一地点,此时服务端返回的大数据就是这一时刻最有效、质量最高的节点。有预调度了,用户如果发生位置或者网络的切换,就不再是最佳节点。

有哪些维度能保证有效性?

第一个,有效性。强制设置有效期,在有效期过了以后会强制重新调度一下队列里面的一些结果。

第二个,网络切换。因为分了两块缓存,一块是WiFi的结果,一块是4G的结果, 基于流量考虑(像4G,通常流量是比较敏感的。)如果网络切换看到4G里面有结果了,把失去有效期结果的重新刷新一遍,有效期的继续保留。(因为认为切回到4G的时候,它的运营商不变。) 切换到WiFi。安卓上面做了热点的对比,切回到同一个热点,逻辑跟4G一样,但是IOS上面拿到这个热点会有些障碍,我们无脑把它全部更新。因为WiFi下用户对流量也不敏感。

1.8 建连优化

服务端建连有三个方面。第一个方面,跟服务器交互的时候首先要跟服务器请求流地址。跟服务器建连的流程为了保证跟建连服务器时间最短,引入了测速机制。(就是刚才说的这个) 回来的时候进行权重排序把服务端建连时间长的节点筛选,不会参与到后续建连。测速机制第二个作用就是筛选节点服务器。第二个方面,建连成功了,但是服务端回包特别慢,会启动灾备逻辑。经过几重维度判断之后会启动切换流程达到灾备的效果。第三个方面,回来的结点还会把原始DNS URL进行最后灾备:所有流都拉不到的时候会启用DNS拉流。这时提前把DNS做预热(因为DNS本身寻址的过程很耗时。而这个时机,因为DNS本身它是阻塞式、不可取消的过程),在整个首屏流程里会有两次更新网络请求。第一次跟调度服务器进行HTTP请求。调度服务器的域名是固定的,可以在APP起来的时候并行开启一个线程去把调度服务器域名进行DNS解析。第二个,域名拿到,在进行NGP调度的时候同步开线程,把当前拉流域名进行DNS解析。这样在同一时间内做了两件事情,走到这个分支就可以直接从DNS缓存里拿IP地址进行建连。

2. 流解析优化首屏空间,解码优化,渲染。

解码的时候FFmpeg里面有几个比较重要的优化参数。

第一个,FFmpeg在拉到流首先会对视频的首包进行解包操作。解包的过程当中会进行流的探测,比如解析到SPS或者PPS,拿到所有的meta信息,再分析每一线长度,这个本身很耗时。针对这种情况优化是直接指定视频格式(一般客户知道自己的视频格式。如果接入自己的转码服务器,通常支持的格式也是主流的FLV或者MP4)这样只要把格式植入进去就完全可以省略FFMpeg解析的时间。

第二个, 探测长度和探测时长(probesize/analyeduration)。这个探测主要的作用是解析当前视频流音轨和视频轨道的信息。采用动态下发机制来设置。理论上来说这两个参数设置越小,首屏时间会越短。但是这也会带来一些其它问题问题,有些异常的流,比如互动直播在一个连上上下麦的过程当中,混流的时候会出现前半段是音频的连麦(音频轨道流), 此时用户以视频形式上麦,混流的时候会同时混音频流和视频流。对于播放器这边来说,播着音频流突然来了个视频流,探测时间如果太短就感知不到这种格式的流,会导致最后解码器开不了。

后来针对这种流解析进行了改进。就是在FFmpeg解析出来有流的Index变化的时候再去开相应的Codeck。

2.1 点播业务级别秒开优化-预加载

在点播有业务级的秒开,类似于划屏滚动,抖音这种滚到什么地方就播放什么地方。这里提供了两种方式,第一种采用多实例,第二种就是采用DataSource。

多实例就是播放器性能。(自测之后发现,播放器性能以iPhone7为例来测,最多可能会同时开十个播放器,完全可以满足这种场景)维护播放器的缓冲池。当用户播放到当前预加载上下的几个,这样用户在不停的滚动当中(数据因为已经加载好了,只需要调用一个播放渲染就可以了)也可以达到秒开的即时感。但是这种情况有一个问题,在低端机型上缓冲池的大小会受限制,因为同时它会开N个实例,这N个实例里面的解码线程,渲染线程也会有N个,在低端机上吃不消。第二,带宽抢占的问题。他们几个同时去加载,上下几个在正在占着带宽,中间这个加载很慢。这种实现方式不是很灵活最根本在播放器。

Data source就是播放器提供了一组接口,第一种要给当前的Content Size。第二就是Play、Stop和Seek这种常规的操作接口。而应用层需要有完善的拉流机制和缓存机制,把所有拉回来的流作为Source data存在队列里面,也可以根据自己的需求去拉某个优先级高的流,进行优先级排序,或者进行其他维度权重的排序。这样整个秒开逻辑只需要用一个播放器就可以了。比如想用Data source 1 只需要按照播放器逻辑塞给播放器,播放器就会进行同步渲染。而如果播放到2(因为数据已经有了)渲染的性能正常的情况下打开是会在秒以内的。而且对低端机的性能要求不严格。

这样做的缺点是大部分逻辑都需要应用层来维护,而应用层拉流机制和缓存机制可能会比较麻烦,但灵活度是最高的。做点播业务级优化的另外一个问题与秒开也是有关的。

2.2 无限循环

无限循环在播放器内部就是播放到最后再Seek到开始。而在Seek过程当中每次都是请求重新建连Socket,发起HTTP请求建连。这种短视频每次发起Socket都会有网络请求、执行加载,用户的体验很不好。

针对这个进行优化,引入了本地缓存,边下边播功能:有两份文件,原始数据文件,对应Map文件。Map文件对应的数据就是原始数据的时间段,因为有可能点播播放到某一个点,Seek到另外一个点,在文件写入的时候中间会有个空洞,用Map就是来记录到这个点跟点之间以及已经记录的视频段。用户播的时候优先从原始的本地的文件里面进行Seek,就可以达到无限循环秒开的优化。

边下边播另外一种优势是真的可以做到用户即播即看,类似于引入了内部管理和外部管理的快播效果。比如说用户想要优化本地缓存。就引入内部管理,视频在播放器完成的时候把缓存消除,整个流程对用户是不可见的。外部管理就是把内部缓存文件的路径以及管理全部都委托给外部,用户在播完的时候会拿到一个原始文件,这种文件根据它设置好的格式重新进行封装后再发给它。

经过优化,去年负荷在经过引入调度之后,秒开率小于五百毫秒大概占43%,大于五百毫秒到一千毫秒有36.72%。整体秒开率能达到75%以上。

2.3 灾备策略

播放器优化过程当中遇到很多坑。比如网络路由变换,在播放器从CDN某一个节点进行拉流的时候路由从4G变到WiFi。经过测试,如果从原节点拉流,可能有三种情况。第一种可以正常拉到流,第二种情况,没有流,CDN直接就把流停了,第三种情况,会发出时间戳异常的流。这三种情况全是CDN的行为没法预期。因此对于网络变更,会引入自动重联的机制。无论当前流是不是可靠,都会自动强制重连一次来保证流的鲁棒性。

CDN服务停止,统计平台会发生报警,通过两种方式:一种服务端调整下发权重,让用户重新拉流;第二种是客户端内部有重联机制。如果重连会依次按照当前CDN节点顺序,依次向下重连。第二个,遇到边缘节点分配错误。这种是比较普遍的,(因为比如说有些之前遇到了一些个小的运营商,他介绍的运营商,在的CDN厂商的一个列表里面并没有备份,比如说它是一个教育网,但是在CDN那边,它可能误认为电信网,这样有可能发生最极端的情况,以前在新疆、西藏那边的一个用户,节点直接给调到了山东,这样用户可能根本拉不到流)在灾备要及时发现并且反馈给CDN,让CDN及时调整节点。第三种,CDN返回视频流有格式错误。因为在推流端有QS机制不停刷新SPS跟PPS信息,而CDN在早期有可能会因为频繁变更而导致它的数据错误,这样就导致播放器无法做到兼容。还有一种情况,在做SEI透穿的时候,有一些CDN会把的SEI信息强制关掉,这样也达不到所要的效果。

服务端灾备核心策略就是尽可能多下发CDN,而CDN里面最可靠的是自己的CDN和原站,通常作为最保险的CDN下发。内部会专门监测CDN的平台质量,它监测平台质量分为两个方面,第一个方面就是会有心跳上报机制定时探测每个CDN,以及CDN下面可能分配结点的活跃度。第二个方面根据播放器上报的卡顿率来评判,实时向调度服务器汇报CDN情况,根据汇报结果综合计算来评判当前CDN到底是不是可靠。

客户端的灾备。在合适的时机进行切流,因为不管怎么样不能让用户产生拉不到流的这种情况。最开始会逐条拉送,CDN下发回来之后会拉第一个地址,第一个地址异常会拉第二个地址,当所有地址都拉不到会从原始地址进行拉流,经过这一系列的灾备再拉不到流,还会依赖于外部设置的重新调度,以及重新重试的机制进行第二次灾备。而在播放过程当中会完全依赖重置机制,这个重置机制对外的接口里面给用户开放了足够的灵活度,比如重试几次或间隔,或者在他需要的时候进行重试。这样做既可以保证拉流成功率,也可以省去应用层开发工作量。

3. 总结

卡顿不是播放器端的问题,播放器能够体现出用户感知卡顿的结果,最终体现的应该是整个视频链路的质量问题,发生卡顿肯定是链路上某个节点有问题。

秒开优化的根本是需要在首屏流程中把耗时的操作移除,这样只要拿到了第一个数据包就马上进行解码渲染,最快给用户呈现画面。

链路质量需要有完整的质量监控体系。第一是监测链路质量。第二,帮助选择最优的链路质量,持续优化过程,以备后面拉流的用户来每次都可以拿到最高质量的流。

在拉流过程当中通过合适的切流措施和多CDN的灾备逻辑来保证拉流成功率。

0 人点赞