欢迎光临
我们一直在努力

04 · WebRTC 低延迟优化实录:出画面从 11.5 秒到百毫秒级

本系列第 4 篇 | 2026-07 | 标签:WebRTC、低延迟、GStreamer、Amazon KVS

视频通路有了(第 1~2 篇),接 WebRTC 推流。方案:VPU 硬编出的 H264 → Amazon KVS WebRTC C SDK → 自建信令(不依赖 AWS 云服务)。第一版功能就通了 —— 然后开始了一段密度最高的优化期:一天之内,出画面从 11.5 秒压到 1.23 秒; 首帧延迟从秒级压到几十毫秒。这篇按时间线复盘每一步,所有数字都是板上实测。

起点:能跑,但没人忍得了

第一版:HTTP 轮询式信令 + 等 ICE 候选收集齐再交换。 症状:从呼叫到出画面要 11.5 秒,而且每次都不太一样。

先立规矩:优化不是猜的。动手前先埋了三个探针,把延迟拆账:

  • 采集延迟探针:帧打上 CLOCK_MONOTONIC 时间戳(pts_us),订阅侧收到时相减, 得到"摄像头→编码→共享内存→WebRTC"的端内耗时
  • 端到端首帧计时:浏览器页面里从发 offer 到第一帧渲染
  • 稳态延迟拆账:RTT / jitter buffer / 解码各占多少

方法论一句话:先把账拆开,再决定动哪一刀。下面每个优化都能用探针复测。

第一轮:信令与候选(11.5s → 591ms)

改动原理效果
HTTP 信令换 websocket + trickle ICE 不等候选收集齐,边收集边交换 出画面 11.5s → 1.23s
摄像头常开 不再等 viewer 接入才起管线(起管线本身秒级) —
viewer 接入时强制 IDR 连接建立后立刻催一个关键帧,不用等下一个 GOP 首帧 1025ms → 591ms

强制 IDR 用 GStreamer 的 force-key-unit 事件实现。实测过 v4l2slh264enc 对 GstForceKeyUnit 的响应:55ms 内出关键帧 —— 完全够用,不需要重启编码器 或改 GOP 这种大动作。

WebRTC 首帧的本质约束:连接建立后,订阅者必须等到一个可独立解码的关键帧 (SPS+PPS+IDR 一个访问单元)才能出画面。所有首帧优化都在回答一个问题: 怎么让这帧最快出现。

第二轮:TURN 的教训

两块实测记录:

  • 板子侧不配 TURN,公网首帧 2801ms → 1476ms
  • 局域网两端都配 TURN 时首帧 866ms,都不配降到 232ms

结论:TURN 的代价是对称的。局域网/直连可达的场景,任何一端都别配; TURN 只该留给真的打洞失败的那一侧。本项目最终形态:板子只配 STUN, TURN 配置下发给浏览器侧。

第三轮:线程模型

三个修复,主题是"别让慢的东西堵住快的":

  • 候选处理挪到 worker 线程(顺带修了一个候选被丢弃的 bug):回调线程里做 重活会阻塞整个信令流程
  • 发送线程解耦:发送队列满时丢帧,绝不让网络背压倒灌进采集/编码线程 —— “网络再烂,采集节奏不动”。这条和第 7 篇音频的"过期数据宁可丢不要发" 是同一个思想的两个面
  • CLOSED 时后台异步拆连接 + PLI→IDR 去抖:断开清理不占调用点, 多个 PLI 合并成一次 IDR 请求

第四轮:决定性的一击(0~1000ms 随机 → 20~40ms)

首帧延迟在 0~1000ms 之间随机抖,复现不了规律。根因: 编码器侧的强制 IDR 请求状态没有复位,只有第一次生效,后续全部被吞。 第一次碰巧赶上 GOP 边界就快,否则等满一个 GOP(1s)。修完首帧稳定塌缩到 20~40ms。

这是整个系列性价比最高的一次修复:一行状态复位级别的改动, 消灭了最大的延迟方差来源。教训:"只生效一次"的功能等于没有功能, 对这类状态机要多打几次验证。

同期的两个正确性修复:

  • 收流侧判断关键帧改成自己扫 Annex-B NAL 头 —— 原来读的是一块未初始化内存, 行为纯看运气(这种 bug 测试时可能永远不复现)
  • 解析浏览器的 mDNS(.local)混淆候选:现代浏览器默认把主机候选混淆成 xxxx.local,不解析就丢了一半候选

第五轮:采集端(230ms → 20ms)

采集延迟探针显示端内就占了 230ms。各种缓冲策略调了个遍没用, 最后发现是初始化脚本 sensor 名字截断导致协商走了另一条路径(第 2 篇坑四)。 修完直采 ~8ms。

再次印证那篇的结论顺序:先验证配置是否真的生效,再怀疑管线结构。

最终架构下的实测基线(2026-08 板测)

拆进程(第 5 篇)+ 主路 libcamerasrc(第 6 篇)之后:

读数局域网公网 5G
采集延迟(摄像头→编码→共享内存→WebRTC) 24~30ms 同左
跨进程强制 IDR 响应 21~34ms 同左
发布者丢帧 0 0
呼叫→浏览器首帧 ~120ms ~690ms
稳态 RTT / fps 2ms / 30 88ms / 29
PLI 次数 1 0(关键帧靠强制 IDR,不用催)

24~30ms 的采集延迟由 libcamerasrc 主导:比裸 v4l2src 的 ~8ms 多出一截, 是 libcamera request 流转 + IPA 跑 3A/AE 的固定代价 —— 我们为了 3A 主动选的 (见第 6 篇),不是回归。

⚠ 测延迟的两条纪律

  • SDK 日志级别必须是 INFO(3) 且只重定向到文件、不要 tee —— DEBUG 日志会把 ICE/DTLS 握手拖慢几百毫秒,tee 同样拖。 开着 DEBUG 测出来的数全是假的。
  • 对照实验用 videotestsrc 替掉相机源,把采集变量隔离干净 (第 6 篇用它定位了 libcamerasrc 的 CPU 归属)。
  • 关键帧自包含:中途接入的唯一前提

    订阅者随时可能接入(WebRTC 中途加入、共享内存新订阅者),它拿到的第一帧必须 SPS+PPS+IDR 自包含。管线靠 h264parse config-interval=-1 + alignment=au 保证。

    反面教材提前预警(第 6 篇细讲):一次重构把这个属性弄丢了(默认 0 = 参数集只在 流头发一次)—— 结果是中途接入黑屏,而启动链路一切正常。这不是优化项, 是正确性前提。

    小结:低延迟清单

  • trickle ICE,别等候选收齐
  • 相机常开 + 接入即催 IDR;确认催帧机制每次都生效
  • TURN 只给真正需要的那一侧
  • 发送与采集解耦,网络背压不上溯
  • 关键帧自包含(config-interval)
  • 探针先行:先拆账,再动刀;测数时关 DEBUG、别 tee

  • 下一篇:05 · 一个摄像头,多个消费者 —— webrtc 和人脸识别都要画面,一颗摄像头一个编码器实例怎么办?

    赞(0)
    未经允许不得转载:171主机测评 » 04 · WebRTC 低延迟优化实录:出画面从 11.5 秒到百毫秒级
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址