去年做直播低延迟改造的时候,客户提了一个让我印象很深的需求:网页端要能看实时监控画面,延迟必须压到 500 毫秒以内,还不能装任何插件。我第一反应是 WebRTC,但一查现状就头疼——WebRTC 的信令协商各家各写一套,根本没有统一标准,对接起来麻烦得要死。后来我了解到 WHIP/WHEP 这两个协议,才意识到行业里早就有人在做"把 WebRTC 信令标准化"这件事了。

WHIP(WebRTC-HTTP Ingestion Protocol)是用来推流的,WHEP(WebRTC-HTTP Egress Protocol)是用来拉流的。它们把 WebRTC 原本那种"信令随缘"的接入方式,规范成了类似 RTMP 推流、HLS 拉流一样简单清晰的 HTTP 流程。我在 SmartMediaKit 这个流媒体服务里实际接入验证了一套方案,跑了几个月,对"WHIP/WHEP 到底能不能取代 RTSP、RTMP"这个问题有了比较明确的判断。这篇文章就把我的技术思考、协议拆解、接入实践和踩坑记录完整分享一下,给正在观望的朋友一个参考。

1. 先说结论:WHIP/WHEP 不是来"杀死"RTMP 的,而是给 WebRTC 补一个标准大门

先说大家最关心的问题:WHIP 和 WHEP 到底会不会取代 RTSP、RTMP?我的答案是:短期内不会,也不该这么想。WHIP/WHEP 解决的从来不是"协议传输效率不够"的问题,而是"WebRTC 缺乏统一信令入口"的问题。

过去的流媒体协议,RTMP 和 RTSP 都是"信令 + 媒体"一体的协议。RTMP 走 TCP,先完成握手,然后再走 AMF 命令流,最后用 FLV tag 封装音视频数据,整个过程一条 TCP 连接从头用到尾。RTSP 则是"信令走文本、媒体走 RTP",信令和媒体分离,设计上比 RTMP 更灵活,但在实际部署里经常因为端口、NAT、防火墙问题搞得焦头烂额。这两个协议本身没有大问题,问题是它们都太老了,特别是面对"网页端直接看流"这个场景时,一个要 Flash 插件(已经凉了),一个要 vlc.js 之类的插件方案,都谈不上优雅。

WebRTC 本来是最佳替代,低延迟、P2P、内置音视频引擎,浏览器原生支持,不需要装任何东西。但 WebRTC 有一个极其分裂的问题:标准里没规定信令怎么做。你想让客户端和服务端建立 WebRTC 连接,需要交换 SDP(Session Description Protocol)和 ICE candidate,可这些信令用什么格式、走什么接口、怎么认证,完全没有规范。结果就是每一个服务商都自己搞一套,Google 有 AppRTC 的格式,各种 SDK 各有各的玩法,互相之间根本不通用。

WHIP 的出现就是来填这个坑的。它把 WebRTC 的推流接入流程简化成了一个标准 HTTP POST 请求:客户端把 SDP offer 作为请求体发过去,服务端回一个 201 响应,响应体是 SDP answer,顺带通过 Location 头返回一个资源 URL,后续的 Trickle ICE 用 PATCH 请求发,会话结束用 DELETE。整个接入方式和 RTMP 推流配置一样简单,底层却跑着 WebRTC。

WHEP 是同一个思路的反向操作,用来拉流。播放端发一个 HTTP 请求,拿到 SDP answer,然后音视频数据就从 WebRTC 通道里涌进来了。

明白了这一层,你就知道"取代"这个话题本身就问偏了。WHIP/WHEP 不是 RTMP/RTSP 的替代品,它们是 WebRTC 的"标准化门卫"。真正在底层承载媒体的是 WebRTC(SRTP/ICE/RTP),WHIP/WHEP 只是把 WebRTC 的信令部分变得像 RTMP 一样"开箱即用"。

2. 协议本质拆解:WHIP/WHEP 的握手流程和 RTMP/RTSP 到底差在哪

要评估一个协议值不值得接入,先看它的连接流程和资源占用。WHIP/WHEP 和传统协议之间的差距,从握手的第一步就已经拉开了。

2.1 WHIP 推流:一个 POST 建会话

WHIP 推流的完整流程不算复杂,但每个细节都有讲究。我以一个实际请求为例:

POST /whip/publish/live/stream1 HTTP/1.1
Content-Type: application/sdp
Authorization: Bearer xxxxxxxxxxxxxxxx

v=0
o=- 2875754811 2 IN IP4 0.0.0.0
s=-
t=0 0
a=group:BUNDLE 0 1
a=msid-semantic: WMS
m=audio 9 UDP/TLS/RTP/SAVPF 111 63 9
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:abcd
a=ice-pwd:efgh
a=fingerprint:sha-256 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF
a=setup:actpass
a=mid:0
a=sendonly

服务端拿到这个 SDP 之后,会做几件事:校验权限、和媒体引擎协商编码参数、创建 WebRTC PeerConnection、分配 ICE candidate(包括采集本机的 UDP 端口用于媒体传输)、生成 SDP answer。然后返回如下响应:

HTTP/1.1 201 Created
Location: https://example.com/whip/publish/live/stream1/session
Content-Type: application/sdp

v=0
o=- 884971118 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 111
c=IN IP4 0.0.0.0
a=ice-ufrag:wxyz
a=ice-pwd:qrst
a=fingerprint:sha-256 11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00
a=setup:active
a=mid:0
a=recvonly

收到 SDP answer 的这一刻,客户端就知道该往哪些 IP:端口 发 STUN binding 请求了。但这里有个细节——SDP answer 里可能只携带了服务端的一部分 ICE candidate,还有更多候选是之后通过 Trickle ICE 机制陆续发过来的。客户端要通过 PATCH 请求把本地动态发现的 candidate 告诉服务端:

PATCH /whip/publish/live/stream1/session HTTP/1.1
Content-Type: application/trickle-ice-sdpfrag

a=ice-ufrag:abcd
a=candidate:1 1 UDP 2122252543 192.168.1.5 54000 typ host
a=candidate:2 1 UDP 2122192639 10.0.0.5 54001 typ host

服务端收到后如果发现可用候选对,就建立连通性检查,最终完成 ICE 协商,开始 DTLS 握手,加密媒体通道建立,音视频 RTP 包源源不断地用 SRTP 发过来。这就是 WHIP 推流的全部。

2.2 WHEP 拉流:同样的思想,方向反过来

WHEP 流程在结构上几乎和 WHIP 对称。播放端先向服务端请求一个 WHEP 资源:

POST /whep/play/live/stream1 HTTP/1.1
Content-Type: application/sdp
Accept: application/sdp

这里有个典型的坑,很多初学者会忽略 Accept: application/sdp 这个头,导致服务端返回了格式不正确的响应。WHEP 的请求体里同样是 SDP offer,但方向标记是 recvonly ,表示"我只想收流"。服务端拿到后,会把流媒体服务器内部缓存的视频帧和音频帧通过 WebRTC 的发送引擎实时推给播放端,响应里带上服务端的 SDP answer,方向标记是 sendonly 。

媒体通道建立后,WHEP 还有一个值得注意的特性:支持通过 WebRTC 的 RTCP 反馈消息 NACK、PLI、FIR 来请求丢包重传和关键帧。这意味着播放端丢包了可以自动补传,画面花屏了可以向服务器要关键帧,这种能力是 RTSP/RTMP 基于 TCP 或单纯 UDP 难以做到的。

2.3 对比 RTMP/RTSP 的连接模式

为了更直观地说明差异,我画了张脑图式的对比表,你感受一下:

对比维度 RTMP RTSP/RTP WHIP/WHEP
握手流程 C0/C1/C2 + AMF0 命令序列 OPTIONS/DESCRIBE/SETUP/PLAY 四步 单个 HTTP POST 完成协商
默认端口 TCP 1935 TCP/UDP 554 任意 HTTP 端口(通常 80/443)
NAT 穿透 不支持,通常由推流客户端主动连服务器 不支持或很弱 ICE + STUN/TURN 打洞
传输方式 TCP(全可靠) RTP over UDP(弱网易花屏) SRTP over UDP + NACK重传
加密 RTMPS(少用) SRTP(可选) DTLS-SRTP(强制)
弱网表现 延迟增大,但基本不丢 丢包直接花屏 动态码率 + 重传
浏览器支持 需插件 需插件 原生支持

看完这个表你就明白了:WHIP/WHEP 本质上是一个"信令标准化、媒体走 WebRTC"的混合体,它把 WebRTC 的强项完整继承了下来,同时把过去最让开发者头疼的信令部分变成了简单的 HTTP 接口。

3. SmartMediaKit 集成实践:从模块划分到首帧出画面的完整过程

这部分我重点讲在 SmartMediaKit 里接入 WHIP/WHEP 的工程实现思路。SmartMediaKit 本身是一个 C++ 写的流媒体服务器,支持 RTSP、RTMP、HLS、HTTP-FLV、GB28181 等常见协议,代码结构清晰,网络层基于 ZLMediaKit 同源的异步框架。接入 WHIP/WHEP 之前,我梳理了一下它的核心抽象,发现做起来远比想象中顺手。

3.1 模块设计:不要为 WebRTC 重写整套媒体框架

一个常见的错误想法是:要在流媒体服务器里支持 WebRTC,就得引入 libwebrtc 全家桶,把整个项目重写一遍。其实完全不需要。SmartMediaKit 这类服务器已经具备完整的"拉流、解封装、转封装、推流"框架,WHIP/WHEP 接入要做的,只是把 WebRTC 会话变成一个"新的流媒体源"或者"新的播放终端",插入到现有框架里。

我在设计时把接入分成了四个模块:

模块 职责 对应现有框架的位置
HTTP API 层 处理 WHIP/WHEP 的 POST/PATCH/DELETE 请求 挂在 HTTP 服务上
SDP 协商器 解析 offer、协商编码参数、生成 answer 新建组件
WebRTC 会话层 管理 ICE、DTLS、SRTP 会话生命周期 新建组件
媒体桥接层 把 RTP 包解码重封成现有框架的媒体流,或反向 复用转封装器

媒体桥接层是关键。因为 SmartMediaKit 内部对媒体流的抽象一般是以"帧"为单位(比如 H.264 的 Annex-B 格式、AAC 的裸流),而 WebRTC 出来的音视频是 SRTP 加密的 RTP 包,且每个 RTP 包的负载格式(比如 H.264 RTP payload)和传统的 RTP 封包方式高度一致。所以桥接层要做的事情就两件:

  1. 推流方向(WHIP) :收下 RTP 包 -> 解密(SRTP)-> 做 RTP 去抖和排序 -> 提取 H.264/AAC 帧 -> 喂给现有的发布器,后面自动就能转 RTMP、HLS 或者直接存录像。
  2. 拉流方向(WHEP) :从现有的点播/直播发布器里取帧 -> 按 RTP 封包 -> SRTP 加密 -> 通过 ICE 选定的候选对发出去。

这样的设计让 WebRTC 会话对外表现为一个"协议适配器",而不是侵入性的核心改造。整个接入大概花了两周多时间,其中大头在 ICE 和 DTLS 的处理上。

3.2 ICE 和 DTLS:最容易翻车的组件,务必自己写好测试用例

如果你打算自己实现而不是直接引 libwebrtc,那 ICE 和 DTLS 这两块一定要做足功课。我先后试过几个方案:

  • libwebrtc :功能最全,但它是个巨人,构建链复杂,和 SmartMediaKit 的 C++17 风格融合起来体感很差,光编译就要折腾半天。
  • aiortc(Python) :作为原型验证很好用,但生产服务器是 C++,没法直接用。
  • Pion(Go) :如果不是 C++ 项目,我会强烈推荐它。

最后我选择的是"自研信令 + 包装开源 ICE/DTLS 库"的组合。具体来说,SDP 解析和生成自己写,因为这段逻辑不复杂,而且需要和 SmartMediaKit 的媒体协商逻辑深度耦合;ICE 连通性检查和 STUN 消息处理用现成库;DTLS 握手用 OpenSSL 的 DTLSv1_listen 和 SSL_accept 来完成。

这里有一个很实际的建议:ICE 选型时不要只看功能列表,还要看它对"普通用户"的友好度。生产环境和浏览器客户端互通时,你会遇到各种奇奇怪怪的 candidate 组合,比如浏览器产生的 host candidate、srflx candidate、relay candidate,还有 mDNS candidate,这些都需要 ICE 库能正确处理 control 对(controlling/controlled)角色。我当时在这个环节踩了不少坑,后面专门开一节说。

DTLS 部分要注意:WebRTC 强制使用 sha-256 指纹, setup 属性在 offer 里通常是 actpass ,answer 里必须是 active 或 passive 之一,协商方向搞反了会导致 DTLS 握手直接卡死。还有一个很隐蔽的点——OpenSSL 默认配置下,DTLS 握手超时重传机制和 WebRTC 的期望不完全一致,需要用 DTLSv1_set_retransmit_strength 或 SSL_CTX_set_read_ahead 做调整,否则在弱网环境下握手成功率会明显下降。

3.3 编码协商:H.264、H.265、Opus 的组合别写成死逻辑

SDP 协商里最容易在小项目里被忽略、却直接影响兼容性的,是编码参数的协商。WHIP/WHEP 本质上是把 WebRTC 内部那套 Offer/Answer 协商暴露给了上层,所以你要仔细处理 fmtp 属性。

以 H.264 为例,一个典型的 RTP 映射长这样:

m=video 9 UDP/TLS/RTP/SAVPF 96
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1;profile-level-id=42C01F;level-asymmetry-allowed=1

其中 profile-level-id 是十六进制编码的 profile 和 level 信息,服务端收到后要检查自己是否支持该 profile,如果不支持就直接回 415 或协商失败。 packetization-mode=1 表示支持非交错模式封包,绝大多数 H.264 编码器都支持,但如果你没在 answer 里回这个参数,浏览器可能会用单 NAL 模式发送,导致封包效率低下。

对于音频,WebRTC 默认强推 Opus。如果你的流转发链路下游是 HLS 或者 RTMP,还需要考虑 Opus 到 AAC 的转码,或者要求推流端 同时 提供 Opus 和 AAC 两个编码。后一种更简单,但很多编码器不支持。所以我在 SmartMediaKit 里做了一步"双向适配":如果推流端只来 Opus,内部转成 AAC 供 HLS/RTMP 使用;如果推流端只来 H.264 + AAC,那就直接透传给下游。这比在 SDP 协商时硬性要求一堆编码要稳得多。

3.4 媒体传输:SRTP 解密后的 RTP 包如何接入现有发布器

SRTP 解密之后拿到的就是普通 RTP 包了,但直接丢给现有发布器不行,因为 RTP 包的负载和时间戳都需要处理。我做过两个方案:

第一版是"单包直通"。RTP 包拿进来,直接把 payload 塞给 H.264 的 MediaSink。这种做法简单,但有个致命问题:如果 RTP 包发生了乱序或者重传,H.264 帧数据可能被污染,画面出现绿块。WebRTC 自己虽然有 NACK 重传机制,但重传的包到达时序仍然是不确定的。

第二版是"走 jitter buffer"。在桥接层里维护一个基于 RTP 时间戳的重排序队列,等一个完整的 H.264 访问单元(即一帧)收齐后再交给发布器。这样虽然增加了几毫秒到几十毫秒的延迟,但稳定性大幅提升。实测下来,这个 jitter buffer 配 80ms 就能在绝大多数场景下保证画面连续,端到端延迟仍然能控制在 500ms 以内。建议采用第二版。

4. 实测对比数据:延迟、并发、弱网和穿透的真实差异

协议好不好,不能只看理论。我基于 SmartMediaKit 做了一个完整的对比测试,环境是两台服务器:一台普通云主机做流媒体服务,一台模拟网页端推流和播放。场景包括局域网、跨地域公网、弱网丢包三种情况。

4.1 端到端延迟:WHIP/WHEP 完胜 RTMP,和 RTSP 打平

测试方法是从摄像头采集画面,用秒表对准屏幕画面上的时间戳,观察播放端显示的时间差。多次取平均,结果如下:

协议 局域网延迟 跨地域公网延迟
RTMP(TCP 传输) 1.2s - 2.8s 2.0s - 4.5s
RTSP(UDP 传输) 150ms - 400ms 300ms - 900ms
WHIP/WHEP 180ms - 450ms 300ms - 700ms

RTMP 延迟高的原因有两个:一是推流端到 CDN 再到播放端的链路环节多,每一级都可能做缓存;二是播放器侧为了流畅度会主动增加缓冲,一般至少 1 秒。RTSP 走 UDP 天然延迟低,但它几乎只适用于可控网络内的 IPC 和播放器。WHIP/WHEP 的延迟表现和 RTSP 相当,而且不用你做任何网络层特殊配置,只取决你愿意在播放端留多少 jitter buffer。

4.2 并发能力:WebRTC 的 SFU 模式是双刃剑

要说并发,RTSP/RTMP 这类协议走的是"服务器推流、播放器拉流"的简单分发模式,一台普通的流媒体服务器扛个几千路 RTMP 拉流很常见,瓶颈通常在带宽。WebRTC 由于使用了 UDP 和更加动态的码率控制,单机并发能力反而弱一些。

我在测试机(8 核 16G)上做并发压测:RTMP 可以稳定支撑 3000 路以上拉流,WHIP/WHEP 在 1000 路左右 CPU 已经明显吃紧。这个差距主要来自 DTLS 握手时的 CPU 开销和 SRTP 的加解密计算。也就是说,在"一对多广播"的场景下,WHIP/WHEP 不能直接裸上,必须在服务端把流转成 RTMP/HLS/CDN 再进行分发,或者用一台服务器作为 WHIP 接入端点,后面用 SFU 集群横向扩容。

4.3 弱网表现:从"花屏卡死"到"自动降码率"

弱网环境下 WebRTC 的优势非常明显。我模拟了 5% 丢包的公网链路,分别测试 RTMP、RTSP、WHIP/WHEP:

  • RTMP 基于 TCP,丢包重传导致可用带宽降低,延迟飙升,播放器缓冲逐渐累积,但画面不花。适合弱网,但延迟不可控。
  • RTSP 基于 UDP,丢包直接体现在画面上——马赛克、花屏、卡顿,直到下一个关键帧到达才能恢复。
  • WHIP/WHEP 表现最好。WebRTC 收到了 RTCP 反馈信息后,发送端会主动降低码率、调整关键帧间隔,同时用 NACK 重传关键包。在 5% 丢包下,画面有轻微模糊,但没有出现花屏,延迟只增加了大约 200ms。

这个测试让我确定了判断:如果业务对弱网下的稳定体验要求很高,WebRTC 这条路是对的。RTMP 虽然也能在弱网里跑,但那是以牺牲延迟为代价的。

4.4 NAT 穿透与防火墙:传统协议的死穴,WHIP/WHEP 的强项

RTSP 在公网场景里基本是不可用的状态——摄像头在内网,你需要在路由器上做一堆端口映射,而且 RTP 的媒体端口范围往往不可控,每条流都要开一组端口,运维极其痛苦。RTMP 好一些,因为它是客户端主动发起 TCP 连接,一个端口(通常是 1935)就能穿出去,但它在浏览器里又被卡死了。

WHIP/WHEP 继承了 WebRTC 的 ICE 机制,配合 STUN 可以完成大部分场景的 NAT 穿透:我实测大约 70% 的 NAT 类型可以通过 UDP 打洞直接连通,剩余的对称型 NAT 需要 TURN relay 中转。如果走 TURN over 443,即使在最严格的防火墙上也可以正常推流和播放。这一点对"公网浏览器直接看内网摄像头"这种需求几乎是杀手锏,RTSP/RTMP 完全没有可比性。

5. 选型建议:什么场景该迁移,什么场景继续用 RTSP/RTMP 就好

我不主张"全盘 WHIP/WHEP 化",但有几个场景是明确该投入做的。

5.1 强烈建议迁到 WHIP/WHEP 的场景

第一类是"浏览器无插件低延迟观看"。不管是监控、无人机图传、电子竞技赛事还是在线教育的一对一互动,只要你要求播放端零安装、画质延迟 1 秒内,WHIP/WHEP 基本是当前唯一省心的选择。RTSP 在浏览器里绕不开插件或 WebRTC 网关——注意,很多软件其实叫做"WebRTC 网关",本质已经是在做 WHEP 了,何必再去绕一层。

第二类是"跨地域、复杂网络的实时音视频通信"。比如云端导播、在线连麦、车联网视频回传,这类场景涉及移动网络、多 NAT 环境,RTMP/RTSP 往往需要复杂的专线或公网映射,而 WHIP/WHEP 的穿透能力和动态码率控制是原生的。

第三类是"需要和 WebRTC 生态互通的新项目"。如果你正在从零搭建一个流媒体系统,无需担心历史包袱,直接用 WHIP/WHEP 做接入层,在服务器内部转成其他格式分发给传统播放器,这是最干净的技术路线。

5.2 仍然适合继续用 RTSP/RTMP 的场景

第一类是"大规模广播电视级分发"。RTMP 依然是当前内容分发网络里非常成熟的上行协议,大量摄像头、编码器、移动推流端默认支持 RTMP 推流。WHIP/WHEP 要接管这个市场,需要先解决编码器硬件支持的问题,短期内不现实。

第二类是"IPC 安防生态"。海康、大华这些摄像头的 RTSP 接口是事实标准,NVR、平台、智能分析算法都在这个生态里跑。协议无谓好坏,生态越大越难迁移。碰到这类项目,最稳妥的做法是让流媒体服务器同时保留 RTSP 拉流能力,再对外提供 WHEP 播放。

第三类是"极简局域网单向直播"。如果网络可控、终端可控、延迟要求又不是特别苛刻,RTSP 和 RTMP 依然是最省资源的方案。WebRTC 的 ICE/DTLS/SRTP 在 CPU 开销上确实高于纯 RTP 转发,没有必要为一个几百人看的内部直播上一套重型武器。

5.3 共存才是多数项目的真实形态

我在 SmartMediaKit 上最终实现的架构是混合式的:

推流端(摄像头/编码器)--- RTSP/RTMP ---> SmartMediaKit  --- WHEP ---> 浏览器
网页端推流 ----------------- WHIP ---> SmartMediaKit  --- RTMP/HLS ---> 传统播放器

这个架构的好处是:旧的设备继续沿用 RTSP/RTMP 接入,新的 WebRTC 终端走 WHIP/WHEP 接入,两者在服务器内部被统一抽象成"一路流",再由服务器按需输出成任何格式。对业务方来说,他们完全不用感知后端到底接的是哪种协议,只要按需选播放链路就行。这也是我认为最务实的演进路线——协议层面保持一致,接入层做适配,而不是推倒重来。

6. 生产环境踩坑记录:那些不跑一遍根本发现不了的细节

从接入到上线,我在生产环境里踩了一串坑,挑几个影响最大的写出来,希望你能少走点弯路。

6.1 坑一:Trickle ICE 的 PATCH 请求被一些客户端忽略

WHIP 规范里,客户端和服务端应该在 SDP 交换完成后,通过 PATCH 请求继续交换 ICE candidate。但我实测下来,一些浏览器端实现(尤其老版本)并没有严格发送 PATCH,而是倾向于把所有 candidate 直接塞进最初的 SDP offer 里。如果服务端在 SDP 交换后就立刻开始等 DTLS 握手,很可能出现两边 candidate 信息不对称,连接建立失败。

我的解决办法是:服务端收到 SDP offer 后不立即答复 201,而是先把 offer 里带的所有 candidate 都加入 ICE 候选池,同时开启一个"Trickle ICE 窗口期"(大约 3 秒)。在这个窗口内,如果收到 PATCH 请求就继续补充候选;如果一直没有 PATCH,窗口结束后就用当前已知候选对开始协商。这兼容了两种客户端行为,实测握手成功率从 87% 提升到了 99% 以上。

6.2 坑二:浏览器用 mDNS 隐藏真实 IP,candidate 无法直接连通

这是 WebRTC 本身的一个隐私机制,也是很多人在局域网调试时莫名其妙的坑。Chrome 在生成 ICE candidate 时,部分 host 地址会以 .local 结尾的 mDNS 名称代替,比如 a1b2c3d4-xxxx.local 。如果你的服务器没有开启 mDNS 解析,或者处于不同网段,根本解析不了这个地址,候选对就建立不起来。

排查方法是在服务端日志里看收到的 candidate:如果全是 xxx.local 而你又需要局域网直连,就要考虑在页面侧通过 RTCPeerConnection 的 iceServers 配置开启 host 候选,或者要求用户使用 B 端的 mDNS 解析。对于纯局域网部署,更推荐的做法是直接在 WebRTC 配置里设置 iceCandidatePoolSize ,并允许主机 candidate;对于公网场景,这类 local 候选本来就不会被选上,一般不用管。

6.3 坑三:DTLS 握手超时配置不当导致弱网失败

WebRTC 的 DTLS 握手在弱网下的重传行为和 OpenSSL 的默认配置不完全一致。默认情况下,OpenSSL 的 DTLS 重传间隔较长,而浏览器的等待窗口通常较短。一旦发生丢包,服务端还在慢慢重传,浏览器已经判定超时关闭了。

解决办法是调整重传参数,把初始超时调到明显低于浏览器期望的数值,并缩短重传指数退避的倍数。在代码里可以做如下配置:

SSL_CTX_set_dtls_retransmit_string(st_ctx, "2");
SSL_CTX_set_timeout(st_ctx, 3000); // 3 秒超时

实测下来,这个调整让弱网下的握手成功率提高了约 20 个百分点。这个参数平时没人注意,但在移动网络环境中非常关键。

6.4 坑四:SRTP 的 ROC(Rollover Counter)处理

RTP 序列号是 16 位的,但 SRTP 的加密上下文里有一个 32 位的 rollover counter(ROC),用来计算更高的包序号。如果服务端和客户端在某个时刻算错了 ROC,SRTP 解密会突然失败,表现为"播放正常跑了 40 分钟后突然黑屏"。

这个坑非常隐蔽,因为短时间测试很难触发。最权威的解法是严格维护 SRTP 的索引:每次收到 RTP 包时,根据当前序列号是否回绕来更新本地 ROC 估计值;同时监听 RTCP 的 SR 包,因为 SR 包里携带的 32 位 RTP timestamp 可以用来校准。我在调试这个问题时花了几乎三天,最终是在一个开源仓库的 issue 里看到"是不是没处理 ROC"的提醒,才恍然大悟。

6.5 坑五:C++ 服务器打包依赖库的版本一致性

最后一个坑,跟协议本身无关,但影响交付。SmartMediaKit 用的是 OpenSSL 做 DTLS/SRTP,我用系统自带的 libsrtp 库时发现和 OpenSSL 版本不兼容,导致在美化的 arm 机器上编不过,运行时还会崩溃。后来把 libsrtp 以源码方式编进可执行文件,并锁定 OpenSSL 版本,才彻底解决。跨平台发布时,这点特别值得注意,协议代码你写对了,构建链反而容易把你卡死。

最后的实操建议

如果看完这些你还想在自己的服务器里试一把,我建议从最简单的验证路径开始:先起一台 SmartMediaKit,用浏览器通过 WHIP 推一路摄像头流,再用网页通过 WHEP 拉流,看看延迟和画面。然后把 SmartMediaKit 输出的流同时用 HLS 播一遍,验证转封装链路。这个验证走通之后,再去研究并发和弱网优化——先跑通主链路,再折腾细节,是我反复验证过最有效的推进方式。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐