欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

优化播放器的秒开体验

最编程 2024-08-13 14:50:13
...

对于视频播放时的画面打开速度,我们可以用下面的指标来衡量:
播放秒开率,指的是播放器开始初始化到视频第一帧画面渲染出来的时间不超过 1s 的次数在总的播放次数中的比例。
播放平均首帧时长,指的是播放器开始初始化到视频第一帧画面渲染出来的平均耗时。

拆解播放器请求视频并播放的过程,我们大致可以分为下面几个阶段:
1)业务侧结合优化
2)DNS 解析
3)TCP 连接
4)HTTP 响应
5)音视频探测
6)媒体封装格式探测
音频编码格式探测(要创建解码器)
视频编码格式探测(要创建解码器)
7)音视频解码
8)缓冲和起播策略
9)渲染

提前加载

播放器必须等到进入直播间请求到直播流地址后才能开始播放,这个时间点其实是可以提前的:我们可以在直播列表页就拿到每个直播间对应的直播流地址,在进入直播间时直接传过去,这样一进入直播间播放器就可以拿着直播流地址开始播放了,省去了从服务器请求直播流地址的时间(虽然这个时间可能没多少)。
甚至,我们可以在直播列表页当滑到一个卡片就让播放器拿着直播流地址预加载,进入直播间时则直接展示画面。
客户端业务侧还可以在进入直播间之前通过 HTTPDNS 来选择网络情况最好的 CDN 节点,在进入直播间时从最好的节点拉取直播流播放从而优化网络加载的时间,加快首屏渲染。

DNS 解析

DNS 解析是网络请求的第一步,在我们用基于 FFmpeg 实现的播放器 ffplay 中,所有的 DNS 解析请求都是 FFmpeg 调用 getaddrinfo 方法来获取的。
DNS 的解析一直以来都是网络优化的首要问题,不仅仅有时间解析过长的问题,还有小运营商 DNS 劫持的问题。采用 HTTPDNS 是优化 DNS 解析的常用方案,不过 HTTPDNS 在部分地区也可能存在准确性问题,综合各方面可以采用 HTTPDNS 和 LocalDNS 结合的方案,来提升解析的速度和准确率。大概思路是,App 启动的时候就预先解析我们指定的域名,因为拉流域名是固定的几个,所以完全可以先缓存在 App 本地。然后会根据各个域名解析的时候返回的有效时间,过期后再去解析更新缓存。至于 DNS 劫持的问题,如果 LocalDNS 解析出来的 IP 无法正常使用,或者延时太高,就切换到 HTTPDNS 重新解析。这样就保证了每次真正去拉流的时候,DNS 解析的耗时几乎为 0,因为可以定时更新缓存池,使每次获得的 DNS 都是来自缓存池。

方案一:IP 直连。
方案二:替换 FFmpeg 的 DNS 实现。

TCP 连接

优化 TCP 建连耗时

TCP 建连耗时在这里即调用 Socket 的 connect 方法建立连接的耗时,它是一个阻塞方法,它会一直等待 TCP 的三次握手完成。它直接反应了客户端到 CDN 服务器节点的点对点延时情况,实测在一般的 WIFI 网络环境下耗时在 50ms 以内,它的时间反应了客户端的网络情况或者客户端到节点的网络情况。
TCP 连接耗时可优化的空间主要是针对建连节点链路的优化,主要受限于三个因素影响:用户自身网络条件、用户到 CDN 边缘节点中间链路的影响、CDN 边缘节点的稳定性。因为用户网络条件有比较大的不可控性,所以优化主要会在后面两个点。可以结合着用户所对应的城市、运营商的情况,同时结合优化服务端的 CDN 调度体系,结合 HTTPDNS 给用户分配更优的连接链路(比如就近接入),从而优化建连耗时。
通过 TCP Fast Open 优化 TCP 建连时长

HTTP 响应

优化 HTTP 响应耗时

HTTP 响应耗时是指客户端发起一个 HTTP Request 请求,然后等待 HTTP 响应的 Header 返回这部分耗时。直播拉流 HTTP-FLV 协议也是一个 HTTP 请求,客服端发起请求后,服务端会先将 HTTP 的响应头部返回,不带音视频流的数据,响应码如果是 200,表明视频流存在,紧接着就开始下发音视频数据。HTTP 响应耗时非常重要,它直接反应了 CDN 服务节点处理请求的能力。它与 CDN 节点是否有缓存这条流有关,如果在请求之前有缓存这条流,节点就会直接响应客户端,这个时间一般也在 50ms 左右,最多不会超过 200ms,如果没有缓存,节点则会回直播源站拉取直播流,耗时就会很久,至少都在 200ms 以上,大部分时间都会更长,所以它反应了这条直播流是是冷流还是热流,以及 CDN 节点的缓存命中情况。
通常 CDN 的缓存命中策略是与访问资源的 URL 有关。如果命中策略是 URL 全匹配,那么就要尽量保证 URL 的变化性较低。比如:尽量不要在 URL 的参数中带上随机性的值,这样会造成 CDN 缓存命中下降,从而导致不断回源,这样访问资源耗时也就增加了。当然这样就失去了一些灵活性。
在播放器请求短视频时,通常会先发起一次 Get 请求来获取短视频的文件长度,然后再根据文件长度来获取数据内容。
如果我们提前获取短视频的文件长度,通过设置 HTTP 请求的 Range 则可以省去第一次 Get 请求来优化首帧时长。

音视频探测

当我们做直播业务时,播放端需要一个播放器来播放视频流,当一个播放器支持的视频格式有很多种时,问题就来了。一个视频流来了,播放器是不清楚这个视频流是什么格式的,所以它需要去探测到一定量的视频流信息,去检测它的格式并决定如何去处理它。这就意味着在播放视频前有一个数据预读过程和一个分析过程。但是对于我们的直播业务来说,我们的提供的直播方案通常是固定的,这就意味着视频流的格式通常是固定的,所以一些数据预读和分析过程是不必要的。在直播流协议格式固定的情况下,只需要读取固定的信息即可开始播放。这样就缩短了数据预读和分析的时间,使得播放器能够更快地渲染出首屏画面。
短视频前置 moov box
播放器在网络点播场景下去请求 MP4 视频数据,需要先获取到文件的 moov box,解析出该文件的编码、帧率等信息后才能开始边下边播。如果 MP4 的 moov box 被放在文件尾部,这种情况会导致播放器只有下载完整个文件后才能成功解析并播放这个视频。对于这种视频,我们最好能够在服务端将其重新编码,将 moov box 转移到靠近文件头部的位置,保证播放器在线请求时能较快播放。比如 FFmpeg 的下列命令就可以支持这个操作:
提前创建解码器
我们还可以在服务端下发业务层数据时就带上直播流或者视频的封装和编码相关信息,基于这些信息,我们可以跳过音视频探测阶段并直接提前创建解码器。
比如,在直播场景服务端可以下发 VideoHeader(包括 SPS、PPS、VPS 等数据)信息,客户端提前初始化解码器。

音视频解码

提前创建解码器

播放器可以创建一个解码器复用池,当解码参数一致时,可以复用解码器。这样一来,业务也可以透传给播放器码流相关的信息,让播放器提前创建解码器来降低播放器首帧渲染时间。
解码器需要的信息通常包括:SPS、PPS、VPS(H.265)。
优化解码器刷新操作
IJKPlayer 播放器在完成音视频探测后,开始进行解码时,如果使用硬解,解码器会在开始做一次刷新解码器的操作,这个操作其实没有必要,但是会有一定的耗时,影响首包到渲染时长。去除这一次刷新操作,首帧时长收益 10-20ms。

缓冲和起播策略

优化 Buffer 填充耗时
缓冲耗时是指播放器的缓冲的数据达到了预先设定的阈值,可以开始播放视频了。这个值是可以动态设置的,所以不同的设置给首屏带来的影响是不一样的。
优化一:调整 BUFFERING_CHECK_PER_MILLISECONDS 设置。
缓冲区填充耗时跟播放器里面的一个设置 BUFFERING_CHECK_PER_MILLISECONDS 值有关,因为播放器 check 缓冲区的数据是否达到目标值不是随意检测的,因为 check 本身会有一定的浮点数运算,所以 ijkplayer 最初给他设置了 500ms 时间间隔去定时检查,这个时间明显比较大,所以会对缓冲耗时有比较大的影响。可以把这个值改小一些。
优化二:调整 MIN_MIN_FRAMES 设置。
另外一个跟缓冲区相关的设置是 MIN_MIN_FRAMES,其对应的使用逻辑在 ffp_check_buffering_l(ffp) 函数中:
优化三:以 audio 缓冲区水位线驱动起播。
有时候会遇到 video packet duration 会有为空的情况,而 IJKPlayer 是以 video 缓冲区水位线来驱动起播的,这样由于有点 video packet 的 duration 为空,会导致为了累积足够的水位下载了实际时长超过水位线的视频数据才开播,这就导致起播较慢,对于这个问题,可以改为:以 audio 缓冲区水位线驱动起播,因为 audio packet 的 duration 通常都是正常的,这样可以优化起播速度。

//
在服务器端可以通过缓存 GOP(在 H.264 中,GOP 是封闭的,是以 I 帧开头的一组图像帧序列),保证播放端在接入直播时能先获取到 I 帧马上渲染出画面来,从而优化首屏加载的体验。

服务端快速下发策略
快速启动优化则是会在 GOP 缓存基本上根据播放器缓冲区大小设定一定的 GOP 数量用于填充播放器缓冲区。
这个优化项并不是客户端播放器来控制的,而是在 CDN 服务端来控制下发视频数据的带宽和速度。因为缓冲区耗时不仅跟缓冲需要的帧数有关,还跟下载数据的速度优化,以网宿 CDN 为例,他们可以配置快速启动后,在拉取直播流时,服务端将以 5 倍于平时带宽的速度下发前面缓存的 1s 的数据,这样的效果除了首屏速度更快以外,首屏秒开也会更稳定,因为有固定 1s 的缓存快速下发。这个优化的效果可以使首屏秒开速度提升 100ms 左右。

提升 HLS 的播放秒开
HLS 的播放秒开分为「直接开播」和「开播 seek」的情况。
1)直接开播
对于「直接开播」的场景,播放起播速度跟播放器的策略有很大的关系。比如 iOS 的 AVPlayer 可能需要下载 3 个 ts 切片才会开始播放。IJKPlayer 则使用水位线策略下载到一定量的数据就能开播,这样相对起播会更快一些。
2)开播 seek
通常我们会用 HLS 来保存直播的回放文件,由于一场直播的时间通常较长,在观看回放时,通常需要 seek 到某一个位置来定位到用户感兴趣的内容。

优化 IJKPlayer 在设置 Surface 时重置解码器的等待时长
在 Android 的实现上,如果 Surface 没有提前创建,IJKPlayer 会先创建一个空转的解码器,解码器会有一个取 buffer 的操作,这个过程有锁,同时会有一个 sleep 时长 100ms。当 Surface 被设置后,IJKPlayer 需要重新配置解码器,这个操作也需要获得前面那个锁,这时候则需要最多等间隔时长 100ms。此外,重新配置解码器也需要几十毫秒。后面解码器创建成功后去从 buffer 取数据时,也会受到前面锁的影响,这时候又需要最多等间隔时长 100ms。
根据这个情况,可以在没有设置 Surface 时,解码器空转的情况下,让线程直接等待,而不进入取 buffer 的操作,防止进入到加锁逻辑,这样可以避免当 Surface 被设置后因为等待锁而造成的延时。

视频本地缓存
加载视频进行播放时,还可以再开一路存储任务,将视频数据缓存到本地,这样当视频下一次再被播放时就可以直接从本地缓存请求数据,一方面可以节省带宽,另一方面可以提升数据加载的速度,从而提升首帧秒开速度。
当然这里的本地缓存需要考虑到如何对视频数据进行分片管理以及当缓存过大时如何对缓存进行清理。

渲染

播放器预渲染
通过预加载视频数据,可以将网络请求的耗时给优化掉,但是播放器还是需要经历解封装、解码、渲染的过程,这个在中低端机器上也会有 100-200ms 的耗时。我们可以通过预渲染来把这些耗时给优化掉。
预渲染是播放器在拿到视频 URL 后就可以开始进行 prepare,在这个过程中会开始读取数据进行解封装、解码和渲染,当首帧渲染处理后就等待后续的 play 指令再进行播放。
预渲染对 CPU、GPU 有额外的消耗,可能会导致 UI 帧率下降,这时候要根据机型性能选择性开启。

预渲染首帧代替封面图
当完成了预渲染的能力,其实可以使用播放器预渲染的首帧代替封面图,这样可以节省封面图下载的流量,也可以降低下载封面图导致的带宽争抢。
但是最终还是要有一个兜底策略,比如:当预渲染未完成时,应该在什么时机选择继续加载封面图。

参考:
https://mp.weixin.qq.com/s/8n8cgsv2o-ziFxK9fJO2Yw