WebRTC 视频管线低延时 & 稳定性全流程优化

作者:lansnow
来源:公众号——好好学习自留地

生产环境真实优化记录:解决 CPU 高占用、WebRTC 有连接无画面、播放卡顿、异常卡死等一系列问题,从现象定位根因,附可复用实战经验。

在 WebRTC 流媒体项目中,经常遇到一类很难复现的诡异问题:信令 ICE 握手成功,但浏览器没有画面;偶发进程卡死;网络波动之后画面持续延时累积。本文基于一套共享内存架构的 Docker 视频管线实战优化,梳理完整排查思路与落地结果。

一、初期现象:连接成功,但是无画面

系统架构:多个 Docker 容器,视频帧依靠 POSIX 共享内存 SHM 流转,支持 RTSP/SRT/WebRTC 三路同时输出。 上线初期出现两个核心现象:multi-output 服务 CPU 占用 76‑88%;WebRTC ICE、DTLS 握手全部正常,Track 状态 opened,浏览器却收不到画面;一段时间后整个进程直接冻结,日志不再输出。

根因定位

持锁调用网络发送,造成死锁(最核心)RTSP 模块内部,互斥锁保护队列的代码块里面直接调用网络发送函数。Docker 网络发生变化时,Live555 线程网络 IO 阻塞,锁一直不释放;编码线程等待锁,整个编码管线彻底卡死。

经验:互斥锁内部只允许做内存数据操作,禁止执行网络 IO、外部回调。

AVFrame 复用 BUG,全部帧被错误标记为 I 帧AVFrame 对象跨帧复用,I 帧标记没有重置,每一帧都被当成关键帧编码,CPU、码率双重浪费。每帧编码前需要显式指定帧类型。

上游输入帧率 57fps 远超目标 30fps,大量无效编码拉高 CPU。增加时钟节拍控速,做帧限流。

JS 层缺少异常重连,断连之后只能手动刷新页面。

第一轮修复完成后,WebRTC 画面恢复,CPU 从 80% 下降至 64%,瓶颈转移到 x264 软件编码。

二、切换 RTSP 摄像头输入,遇到新坑

把数据源从本地视频文件切换为海康 RTSP 摄像头,实时流和本地文件逻辑完全不同:FFmpeg 内部缓冲很容易堆积旧帧,延时会越跑越大。

落地三层低延时防积压策略:

  • 拉流层:RTSP 使用 TCP 传输,关闭内部 RTP 重排队列,缩短探测时间,设置超时快速感知断线。
  • 解码层:开启低延时模式,关闭 B 帧,禁用会引入缓存的多线程解码。
  • 业务层追实时边缘:对比 PTS 时间戳和系统墙钟时间,当帧滞后超过 0.5s 直接丢弃旧帧,只处理最新帧;断线支持指数退避重连。

踩坑点:共享内存结构体定义分散在 3 个代码文件。只修改一处分辨率,其他服务读取内存布局错位,出现 “连接成功但是没有帧”。 教训:跨进程共享的数据结构,必须使用唯一的头文件来源,最好增加运行时一致性校验。

另外遇到海康摄像头安全锁定:密码多次错误后,端口可连通但是拒绝 RTSP 请求,等待约 30 分钟自动解除。

三、全链路打点,找到真实性能瓶颈

主观说 “卡顿、延时高” 没有意义,需要把整条链路每个环节耗时量化。 利用多容器共享宿主机系统时钟,在 SHM 帧结构里透传多组微秒级时间戳:写入 SHM 时间、读取 SHM 时间、入队列、编码开始、编码耗时;浏览器通过getStats()统计 jitter buffer、解码耗时。

2560×1440@25fps 实测瓶颈:

x264 软件编码32ms,单帧绝大部分耗时消耗在这里,已经逼近 40ms 的帧间隔上限,帧率上不去;

SHM 轮询 sleep 30ms,造成平均 18ms 无谓等待;

sws_scale 做同尺寸无效格式转换,消耗 12ms。

针对性做四项优化:

开启 NVENC 硬件编码,注意 Docker compose 的 device 配置,capabilities必须带上video,否则会报 libnvidia‑[encode.so](encode.so) 加载失败。编码耗时从 32ms 下降到 7.5ms。

SHM 读取轮询从 30ms 降低至 5ms,降低等待延迟。

同分辨率直接拷贝 YUV 平面,绕过 sws_scale 无效转换。将固定间隔丢帧改为令牌桶 pacing,允许短时帧到达突发,避免误丢帧。

优化后:服务端 SHM 到发送总延时 24ms,浏览器稳定 25‑26fps,multi‑output CPU 从 107% 下降到 48%。

四、隐蔽故障:做好自愈逻辑,不依赖人工重启

即便延时、帧率达标,长时间运行仍会出现画面卡死,定位 3 个隐蔽问题:

摄像头 RTP 时间戳基准跳变:TCP 连接不断,摄像头内部流重启,PTS 初始值突变。旧逻辑把所有帧判定为过期帧全部丢弃。

修复:检测 lag 超过 5s,自动重置时间锚点。

WebRTC 重连信令竞态:异步回调和主线程对同一个状态标志操作,标志重置时机错误,重连流程超时失败。

经验:异步触发前完成状态重置,不要在回调之后再改标志。

SHM 文件重建,老进程仍然持有旧 mmap 映射,读不到新数据。

服务端:检测 global_seq 长时间不更新,自动 unmap、重新打开共享内存。 浏览器:增加帧看门狗,ICE 连接正常但 6 秒没有收到帧,强制完整重连。

经过故障注入测试(docker pause 模拟进程卡死),整套自愈链路可以自动恢复播放,无需人工干预。

五、核心实战总结

锁内严禁网络 IO,死锁很多时候不是复杂逻辑,而是编码细节失误。

实时流的核心思路是允许丢旧帧,保住最新帧,积压历史帧没有业务价值。

不要凭感觉评估卡顿,做全链路时间戳打点,把每个环节耗时量化。

“连接成功 ≠ 数据流正常”,除了信令状态,一定要做独立的数据流活性看门狗。

跨进程共享内存结构,保证头文件单一来源,避免多处副本不一致。

2K 以上分辨率,软件编码很容易触达耗时天花板,硬件 NVENC 是刚需。

写完自愈逻辑,一定要做故障注入测试,不能只靠正常流程自测。

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

赞 (0)

相关推荐