开篇
先说结论:
字节跳动就曾给出过一份数据:对一部分型号的 Android 手机,播放首帧时长从平均 170ms 优化到 100ms,带来了 0.6% 左右的用户播放时长提升。
衡量指标:
- 播放秒开率,指的是播放器开始初始化到视频第一帧画面渲染出来的时间不超过 1s 的次数在总的播放次数中的比例。
- 播放平均首帧时长,指的是播放器开始初始化到视频第一帧画面渲染出来的平均耗时。
回归探讨,首先要搞明白从数据到播放器都经历了什么,再逐个讨论优化点。
我们大致可以分为下面几个阶段:
- 业务侧结合优化
- DNS 解析
- TCP 连接
- HTTP 响应
- 音视频探测
- 音视频解码
- 缓冲和起播策略
- 渲染
业务侧结合优化
首先要看客户端上进入直播间的业务场景是什么样的?一般而言,都是从一个直播列表页面,点击某一个直播卡片(Cell)即进入直播间。
优化点:
1. 提前获取直播流地址
2. 结合 HTTPDNS 选择最佳 CDN 节点
3. 使用 URL 替代 VID
- 将视频 URL 直接封装在 Model 中,避免多一次请求 URL 的时间开销。
4. 上下滑短视频提前加载播放器:
5. 封面图清晰度降级:
- 当预加载、预渲染等优化做的较好时,封面图可以适当降低清晰度,节省带宽和流量。
- 可以优先做预加载预渲染,在必要时才加载封面图。
DNS 解析
获取到直播流地址,需要进行dns解析得到www.ip+port/资源路径。这步优化就是构建dns缓存池,避免去运营商解析这个过程。DNS解析可以在几十毫秒到几百毫秒内完成,本地的话可以保证稳定再几十毫秒。
1. 优化 DNS 解析过程:
-统计 DNS 解析耗时。没有缓存命中时,DNS 解析通常需要 300ms 以上的时间。有缓存命中则耗时很短。 DNS 解析耗时较长的原因是需要递归到根域名服务器查询。
//libavformat/tcp.c 文件中的 tcp_open
int64_t start = av_gettime();
if (!hostname[0])ret = getaddrinfo(NULL, portstr, &hints, &ai);
elseret = getaddrinfo(hostname, portstr, &hints, &ai);
int64_t end = av_gettime();
- 没采用ipv6地址,需要设置只查询ipv4。
hints.ai_family = AF_INET;
- 采用 HTTPDNS 和 LocalDNS 结合的方式可以提升 DNS 解析速度和准确率。在 App 启动时预解析热门域名,缓存在本地。
2. 实现 HTTPDNS:
- 方案一是 IP 直连,将域名解析到的 IP 直接嵌入到 URL 中。(建议使用nginx作为中间层即可避免。)但存在两个问题:
1. 如果服务端采用 302/307 跳转,客户端依然IP 直连,造成客户端没正确使用跳转ip导致url资源定向错误。
2. 使用 HTTPS 时,证书验证会失败。
AVDictionary **dict = ffplayer_get_opt_dict(ffplayer, opt_category);
av_dict_set(dict, "headers", "Host: www.example.com", 0);
- 方案二是替换 FFmpeg 的 DNS 实现,自行实现 HTTPDNS 解析逻辑。常见的做法是做一层播控服务,客户端请求播控服务获取到实际的播放地址以及各种其他的信息,然后再走 IP 直连就没问题。
//tcp.c 中 getaddreinfo
if (my_getaddreinfo) {ret = my_getaddreinfo(hostname, portstr, &hints, &ai);
} else {ret = getaddrinfo(hostname, portstr, &hints, &ai);}
3. 提升 HTTPDNS 的有效率:
- 在网络切换、滑动场景、内部刷新操作时,之前缓存的 HTTPDNS IP 可能会失效。
- 可以实现一个轮询机制,定期更新 HTTPDNS IP 的缓存,保证 IP 的有效性。
- 同时要处理各种网络切换或内部刷新时更新 IP 缓存的情况。
DNS 优化是网络优化的重点,通过 HTTPDNS 和 LocalDNS 结合使用,可以大幅降低 DNS 解析耗时,从而优化首屏时间。
TCP 连接
1. 优化 TCP 建连耗时:
- TCP 建连耗时反映了客户端到 CDN 服务器节点的点对点延时情况。
- 它主要受限于三个因素:用户网络条件、用户到 CDN 边缘节点链路、CDN 边缘节点的稳定性。
- 可以结合用户所在城市、运营商情况, 优化 CDN 调度, 使用 HTTPDNS 给用户分配更优的连接链路, 从而优化建连耗时。
2. 通过 TCP Fast Open (TFO) 优化 TCP 建连时长:
- TFO 是对 TCP 三次握手的简化, 可在握手过程中交换数据, 减少一次 RTT。
- TFO 流程包括:首次请求获取 Cookie, 后续连接携带 Cookie 进行验证和快速建连。
- TFO 可减少 15% 的 HTTP 传输延迟, 全页面下载时间平均节省 10%, 最高可达 40%。
3. 通过 TCP 预连接和连接复用优化建连时长:
- 缓存 IP 对应的 Socket 连接, 提供预连接接口给业务层使用。
- 业务层可对下一个直播间提前做预连接, 真正拉流时复用这个连接。
- 可对高频域名持续预连接, 提高预连接命中率。需注意网络切换时刷新缓存。
4. 避免首帧网络带宽争抢:
- 在实现第三点基础上,且短视频上下滑场景, 快速滑动时可能会导致预加载的视频数据未被使用, 反而占用了后续视频的首帧带宽。
- 可检测到快速滑动时, 及时中断预连接的 Socket, 避免带宽争抢。
HTTP 响应
优化 HTTP 响应耗时:
-
如果请求有缓存命中, 响应时间一般在 50-200ms 左右。
提升 CDN 边缘节点命中率:
-
避免在 URL 参数中带有随机值, 否则会降低缓存命中率。
-
服务端可对热门内容进行预热, 提高边缘节点的缓存命中率。
优化短视频第一次 Get 请求:
-
设置 HTTP 请求的 Range =文件长度 来省去第一次 Get 请求, 优化首帧时长。
音视频探测
5.1 优化音视频流探测耗时
- 直播业务中,视频流格式通常是固定的,无需复杂的探测过程:
- 可以在播放器中直接读取固定的信息,开始播放,无需经过耗时的 avformat_find_stream_info() 函数。
- 针对 avformat_find_stream_info() 函数的优化:
- 在外部设置较小的 probesize 和 analyzeduration 参数,这两参数决定控制该avformat_find_stream_info 读取的数据量大小和分析时长,频率越高,延时越低。av_dict_set_int(&ffp->format_opts, "fpsprobesize", 0, 0);
- 服务端标准化转码,确定视频格式,计算出最小的 probesize 和 analyzeduration。
- 直接修改 avformat_find_stream_info() 的实现逻辑,针对固定格式进行优化。
- 尝试去掉 avformat_find_stream_info() 步骤,自定义初始化解码环境。
5.2 短视频前置 moov box
- 在网络点播场景下,如果 MP4 视频的 moov box 在文件尾部,会导致播放器需要下载完整个文件才能开始播放。
- 可以在服务端使用 FFmpeg 将 moov box 移动到文件头部,优化播放体验:
ffmpeg -i bad.mp4 -movflags faststart good.mp4
5.3 提前创建解码器
- 对于直播场景尤其有效,因为视频格式比较固定
音视频解码
6.1、提前创建解码器
播放器可以创建一个解码器复用池,当解码参数一致时,可以复用解码器。这样一来,业务也可以透传给播放器码流相关的信息,让播放器提前创建解码器来降低播放器首帧渲染时间。
解码器需要的信息通常包括:SPS、PPS、VPS(H.265)。
6.2、优化解码器刷新操作
IJKPlayer 播放器在完成音视频探测后,开始进行解码时,如果使用硬解,解码器会在开始做一次刷新解码器的操作,这个操作其实没有必要,但是会有一定的耗时,影响首包到渲染时长。去除这一次刷新操作,首帧时长收益 10-20ms。
缓冲区和起播策略
7.1 优化 Buffer 填充耗时
ffmpeg优化参数:BUFFERING_CHECK_PER_MILLISECONDS 减少检查缓冲区填充情况的时间间隔,从 500ms 降低到 50ms,可以减少 200ms 左右的缓冲耗时。
MIN_MIN_FRAMES 最小帧处理。从 10 帧降低到 5 帧,首屏时间可减少 300ms 左右,且卡顿率只上升 2%。
7.2 流媒体服务器侧 GOP 缓存
具体gop指标,可参考下列文章设置。
基于 http-flv 的端到端延迟优化-CSDN博客
7.3 服务端快速下发策略
- CDN 服务端可以配置快速启动模式,在拉取直播流时以 5 倍于平时的带宽速度下发前 1 秒的数据,优化首屏秒开速度。速度提升 100ms 左右
7.4 提升 HLS 的播放秒开
- 直接开播:播放器策略影响大,如 iOS AVPlayer 需要 3 个 ts 切片才开始播放,IJKPlayer 使用水位线策略更快。
- 开播 Seek:IJKPlayer 支持 seek-at-start 能力,可直接下载目标位置数据而不需从头加载;服务端可以根据内容打点切成多个 m3u8,优化 Seek 响应速度。
- 解决 HLS Seek 黑屏问题:stream 的 first_timestamp 未正确初始化,导致无法找到 Seek 位置;IJKPlayer 需要解决跨断层 ts Seek 问题。
7.5 IJKPlayer 优化
设置 Surface 时重置解码器的等待时长
- 在没有设置 Surface 时,让解码器线程直接等待,而不进入加锁的取 buffer 操作,避免后续设置 Surface 时因等待锁而造成的延迟。
7.6 视频预加载
- 提前下载一部分视频数据,达到快速起播的目的。但需要结合业务场景,调试出最合适的预加载策略。
7.7 视频本地缓存
- 将视频数据缓存到本地,下次播放时可直接从本地请求,节省带宽,提升首帧秒开速度。
- 需要考虑视频数据分片管理和缓存清理策略。
综合运用上述优化手段,可以大幅提升播放器的首帧渲染和秒开性能。
渲染
8.1 播放器预渲染
-
在拿到视频 URL 后就开始进行 prepare 操作,进行解封装、解码和渲染,待首帧渲染完成后即可等待 play 指令播放。
-
预渲染可以优化掉解封装、解码和渲染的耗时,但会给 CPU 和 GPU 带来额外消耗,需要根据机型性能选择性开启。
-
如果同时开启了预加载,还需要进行策略优化,协调好预加载和预渲染的时机和流程。
8.2 预渲染首帧代替封面图
-
使用播放器预渲染的首帧来代替传统的封面图,可以节省封面图下载的流量,降低下载封面图导致的带宽争抢。
-
但需要有一个兜底策略,比如当预渲染未完成时,再选择加载封面图。
总结
结合公司实力进行优化,优先考虑缓存提速比如dns http这类连接耗时,tcp改进为fast open,cdn gop缓存机制这个较为重要。其次ijkplay源码,流媒体更改为格式固定版本,去除协议探测耗时。其次考虑源码参数,比如播放器缓存队列启动大小,引入池化技术。
参考文献
播放器秒开优化丨音视频工业实战-腾讯云开发者社区-腾讯云
学习资料分享
0voice · GitHub