提示

返回博客列表

WebRTC 不只是"浏览器能视频通话":信令、SFU、TURN 与录制

客户要做"低延迟互动直播"(老师能跟学生实时互动)。评估下来只有 WebRTC 能满足延迟要求(HLS 十几秒、LL-HLS 两三秒、WebRTC 几百毫秒)。

我们一开始想得比较简单:"用浏览器的 WebRTC API,前端连后端就行"。做下来发现难的全在 API 之外:

  1. NAT 穿透:约 20% 的用户连不上(对称 NAT、企业防火墙),必须部署 TURN 中继;
  2. 拓扑选错:一开始用 mesh(P2P),超过 4 个人就崩——必须上 SFU;
  3. 录制:WebRTC 没有"录制"这个概念,要自己实现;
  4. 延迟没达标:虽然协议是低延迟的,但配置不当照样 3 秒。

这篇写完整的工程方案和踩坑记录。

TL;DR:WebRTC 只定义了媒体传输,信令要自己实现。三个关键决策:拓扑用 SFU(mesh 只适合 ≤4 人,MCU 太重);必须部署 TURN(coturn,约 15~25% 的流量需要中继,不部署就是这部分用户连不上);录制要在 SFU 侧做(客户端录制不可靠)。服务端开源方案推荐 mediasoup / LiveKit / Janus。延迟优化看:getStats() 里的 jitterBufferDelay 和 packetsLost,以及是否开了 simulcast。别用 WebRTC 做大规模一对多直播——那个场景 HLS/LL-HLS 更合适。

目录

一、WebRTC 定义了什么,没定义什么

定义了 没定义
媒体传输(SRTP over UDP/DTLS) 信令(signaling)
NAT 穿透(ICE / STUN / TURN) 房间管理、用户管理
编解码(VP8/VP9/H264/AV1、Opus) 录制
带宽估计、拥塞控制 服务端架构
数据通道(DataChannel) 鉴权

信令必须自己实现(通常用 WebSocket):交换 SDP(会话描述)和 ICE 候选。

一个最小的信令流程:

A 创建 PeerConnection → createOffer → setLocalDescription → 通过 WS 发给服务端 → 转发给 B
B setRemoteDescription → createAnswer → setLocalDescription → 发回 A
双方交换 ICE 候选(onicecandidate)→ 建立连接

好消息:现成的服务端框架(mediasoup、LiveKit、Janus)都帮你封装好了,你只需要对接它们的 API。

二、三种拓扑:Mesh / SFU / MCU

拓扑 原理 上行带宽(每人) 适合人数 服务端负载
Mesh(P2P) 每个人直接连其他所有人 N-1 路 2~4 人 无(除了信令)
SFU 每人只发给服务器,服务器转发 1 路 几人到几百人 转发,CPU 低
MCU 服务器合流后发给每人 1 路 几十人 合流,CPU 高

Mesh 的问题:N 个人,每人要上传 N-1 路。4 人会议每人上传 3 路(3×2Mbps = 6Mbps),很多家庭宽带上行扛不住。超过 4 人就崩。

SFU 是主流选择:

       ┌───────── SFU ─────────┐
       ↓      ↓      ↓      ↑
      A      B      C      D

每人只上传 1 路,服务器负责转发给其他人。服务端不做转码(只转发),所以 CPU 开销小,可以支持很多人。

MCU 会把所有人的画面合成一路再发——CPU 开销大(要解码+合成+编码),但客户端负担最小。适合"老设备参会"的场景。

我们的选择:SFU(默认)+ 可选 MCU(录制时或者低端设备场景)。

三、NAT 穿透:STUN 和 TURN

为什么需要:大部分设备在 NAT 后面(家里路由器、公司防火墙),没有公网 IP,无法直接被连接。

技术 作用 代价
STUN 让设备知道自己的公网地址和端口(打洞用) 几乎免费(只查询)
TURN 当 P2P 打洞失败时,服务器中继所有流量 带宽成本(所有流量经过服务器)

关键数据:实际环境里大约 70~85% 的连接可以 P2P 成功,15~30% 需要 TURN 中继(对称 NAT、企业防火墙、某些移动网络)。

不部署 TURN 的后果:那 15~30% 的用户根本连不上,而且你很难从反馈里定位(用户只会说"连不上")。

部署 coturn:

# 安装
apt-get install coturn

# 配置 /etc/turnserver.conf
listening-port=3478
tls-listening-port=5349
fingerprint
lt-cred-mech
realm=your.domain.com
user=webrtc:your_password_here
min-port=49152
max-port=65535
external-ip=你的公网IP
no-multicast-peers

启动:

systemctl enable --now coturn

前端配置:

const pc = new RTCPeerConnection({
    iceServers: [
        {urls: 'stun:stun.l.google.com:19302'},
        {
            urls: ['turn:your.domain.com:3478', 'turns:your.domain.com:5349'],
            username: 'webrtc',
            credential: 'your_password_here'
        }
    ]
});

安全注意:

  1. 用 lt-cred-mech(长期凭证)+ 动态生成凭证,不要写死用户名密码在前端(会被拿去白嫖你的带宽);
  2. 生产环境建议用 REST API 模式(coturn 支持),服务端按会话签发临时凭证(有效期几分钟);
  3. 限制带宽和端口范围,避免被滥用做中继跳板;
  4. external-ip 要正确(在 NAT 后的服务器必须配)。

四、服务端方案怎么选

方案 语言 特点
mediasoup C++ / Node 性能极好,API 偏底层,灵活
LiveKit Go 开箱即用(有 SDK、有云服务),文档好
Janus C 老牌、稳定,插件式
Pion Go 纯 Go 库(不是服务),适合自己造轮子
Kurento(已停止维护) Java 不推荐新项目用

我的建议:

  • 想快速上线 → LiveKit(它把房间、权限、录制、simulcast 都封装好了,还有现成的前端 SDK);
  • 要极致性能和定制 → mediasoup;
  • Go 技术栈且想自己控制 → Pion;
  • 老项目维护 → Janus。

我们用 LiveKit 做的第一个版本,两天就跑通了 demo,一周内上线了 20 人规模的互动课。选对框架比自己造轮子重要得多——WebRTC 的坑太多(ICE 状态机、编解码协商、simulcast 处理),自己实现会淹死在细节里。

五、录制:WebRTC 最难的一环

为什么难:WebRTC 是"实时传输",没有"文件"的概念。要录制就得在服务端接住 RTP 流 → 解码/转封装 → 写文件。

三种做法:

做法 1:服务端合成录制(LiveKit / mediasoup 自带)

LiveKit 有 Egress 服务,可以把房间录制成 MP4/WebM 或者推到 RTMP:

# egress 配置示例(概念性)
egress:
  outputs:
    - type: file
      filepath: /recordings/room_{room_id}.mp4
      video: {codec: h264, width: 1280, height: 720, fps: 30}
      audio: {codec: aac}

推荐——它处理了合流、时间戳、 discontinuity 这些细节。

做法 2:用 GStreamer / ffmpeg 收 RTP

# 用 ffmpeg 收 RTP 并录制(示意)
ffmpeg -protocol_whitelist file,udp,rtp -i rtp_session.sdp \
  -c:v libx264 -preset veryfast -c:a aac out.mp4

需要构造 SDP 描述,并且要处理 RTP 的丢包、乱序、时间戳跳变——很麻烦,除非有现成封装,否则不建议自己做。

做法 3:客户端录制(MediaRecorder)

const recorder = new MediaRecorder(stream, {mimeType: 'video/webm;codecs=vp9'});
recorder.ondataavailable = e => chunks.push(e.data);
recorder.onstop = () => upload(new Blob(chunks));

不推荐用于正式交付:

  • 用户关掉页面就断了;
  • 性能差(客户端还要编码一路);
  • WebM 格式很多播放器/剪辑软件支持有限;
  • 每个客户端录的质量不一样。

但可以做一个"本地备份",作为服务端录制的补充。

我们的方案:服务端 Egress 录制为主(输出 MP4/H264),客户端不做录制。录制完成后自动转码出 HLS 点播版本(跟第 55 篇的"直播转点播"是同一套流程)。

六、和 RTMP / HLS 互通

常见需求:WebRTC 的音视频要转给传统 CDN(RTMP/HLS)分发给大量观众。

WebRTC(低延迟互动,少数人)→ SFU → 转封装 → RTMP → CDN → HLS(高延迟,大量观众)

实现方式:

  • LiveKit 的 Egress 支持直接推 RTMP;
  • 或者用 GStreamer/ffmpeg 把 WebRTC 的输出转成 RTMP;
  • 转封装时要注意转码(VP8/VP9 → H.264),因为 CDN 和大部分播放器只认 H.264。

这条链路的价值:少数人用 WebRTC 互动,大量观众用 HLS 观看——这是互动直播的标准架构,兼顾了延迟和成本。

七、延迟:为什么还是 3 秒

WebRTC 的理论延迟是几十到几百毫秒,但我们第一版实测 2~3 秒。排查下来是这几个原因:

原因 现象 解决
Jitter buffer 太大 网络抖动时缓冲变长 调小(但要权衡卡顿)
服务端在"中转"而不是"直连" 明明可以 P2P 却走了 TURN 检查 ICE 策略
编码耗时 编码器慢 用硬件编码/低延迟 preset
服务端转发排队 服务器负载高 扩容/优化
我们自己的业务链路 服务端做了额外处理 精简链路

定位方法(见下节):看 getStats()。

优化后:同一机房内 200~400ms,跨地域 400~800ms。

八、监控与排错

getStats() 是 WebRTC 的"万用表":

async function collectStats(pc) {
    const stats = await pc.getStats();
    const result = {};
    stats.forEach(report => {
        if (report.type === 'inbound-rtp') {
            result.inbound = {
                bytesReceived: report.bytesReceived,
                packetsLost: report.packetsLost,
                jitter: report.jitter,
                framesDecoded: report.framesDecoded,
                jitterBufferDelay: report.jitterBufferDelay,        // ← 关键
                jitterBufferEmittedCount: report.jitterBufferEmittedCount,
            };
        }
        if (report.type === 'candidate-pair' && report.state === 'succeeded') {
            result.pair = {
                currentRtt: report.currentRoundTripTime,
                availableBitrate: report.availableOutgoingBitrate,
                localCandidateType: report.localCandidateId,        // 看是不是 relay
            };
        }
    });
    return result;
}

几个关键指标:

指标 说明 异常判断
packetsLost 丢包数 > 2% 影响质量
jitter 抖动 > 30ms 要关注
jitterBufferDelay / EmittedCount 平均缓冲延迟 这就是"延迟"的主要来源
currentRoundTripTime RTT > 200ms 体验下降
是否走了 relay candidate 类型是 relay 说明走了 TURN 走 TURN 延迟必然高

常见问题排查:

现象 排查
完全连不上 ICE 失败 → 检查 TURN 配置、防火墙 UDP 端口、证书
单边能看(一方黑屏) SDP 协商问题、编解码不匹配
频繁断开 网络切换、TURN 超时、服务端会话超时
卡顿 带宽不足 → 开 simulcast、降码率
回声 浏览器 AEC 没生效(通常是用了非原生采集)

一定要把统计数据上报(类似前面 QoE 那篇的做法)——没有数据,WebRTC 的问题根本没法查,因为用户的网络环境你完全看不见。

九、什么时候不该用 WebRTC

WebRTC 的代价:

  • 服务端成本高(SFU 要带宽 + 有状态);
  • 录制复杂;
  • 兼容性问题多(不同浏览器/版本行为有差异);
  • 调试困难。

不适合的场景:

场景 更合适
一对多的大规模直播(几千人看) HLS / LL-HLS(CDN 分发,成本低得多)
纯点播 HLS / MP4
对延迟不敏感(>10s 可接受) HLS

判断标准:

需要 < 1 秒的双向互动 → WebRTC
需要 < 3 秒的单向直播 → LL-HLS
需要 < 10 秒 → HLS(短切片)
延迟无所谓 → HLS

混合架构(最实用):互动的人(< 20)用 WebRTC,观看的人(成千上万)用 HLS。这是我们用得最多的方案。

十、坑清单

  1. 以为 WebRTC 自带信令 → 没有,自己实现(或用框架)。
  2. 用 mesh 做多人 → 超过 4 人就崩。用 SFU。
  3. 不部署 TURN → 15~30% 用户连不上,而且反馈是"不知道为什么"。
  4. TURN 凭据写死在前端 → 被盗用刷带宽。用临时凭证(REST API)。
  5. external-ip 没配 → 在 NAT 后的服务器无法正常工作。
  6. 防火墙没开 UDP 端口范围(49152-65535)→ ICE 失败。
  7. 自己写 SFU → 淹死在 ICE/编解码/simulcast 细节里。用现成框架。
  8. 录制用客户端 MediaRecorder → 不可靠。服务端录制。
  9. 录制输出 WebM 直接交付 → 很多播放器/剪辑软件不支持。转成 MP4/H264。
  10. 不看 getStats → 出问题没法定位。采集并上报。
  11. 不区分"P2P 成功"和"走了 TURN" → 以为延迟低但实际很多人走中继。
  12. 用 WebRTC 做大规模直播 → 成本高得多。那个场景用 HLS。
  13. 忽略移动端差异 → iOS/Android 的 WebRTC 实现有差异(尤其 iOS Safari)。
  14. 编解码协商失败(VP8 vs H264)→ 一端黑屏。确认双方都支持的编码。
  15. 服务端有状态但没做会话清理 → 房间泄漏,内存/连接数暴涨。
  16. 没做带宽自适应 → 弱网下全员卡死。开 simulcast 或 SVC。

最后说说这次做 WebRTC 项目的整体感受。

WebRTC 是那种"demo 半天能跑通,生产要两个月"的技术。

浏览器的 API 设计得很好,跑通一个一对一通话可能就几十行代码。但从"能跑"到"能用",中间隔着一整套基础设施:

  • TURN 服务器(要部署、要防滥用、要监控带宽成本);
  • SFU 集群(要有状态、要扩缩容、要会话管理);
  • 录制链路(要处理转封装、合流、存储);
  • 监控系统(要采集 getStats、要能做问题定位);
  • 兼容性处理(不同浏览器行为差异)。

所以我的第一个建议永远是:用成熟框架(LiveKit / mediasoup),不要自己从 WebRTC 原生 API 造轮子。 框架帮你解决了上面 80% 的问题。

第二个建议是:先想清楚是不是真的需要 WebRTC。很多时候客户说"要低延迟",但实际上"能跟主播互动"才是真需求——而这个需求可以拆成:

  • 主播/嘉宾(几个人)→ WebRTC(真低延迟);
  • 观众(几千人)→ HLS(低成本)。

把需求拆开之后,成本能降一个数量级。

最后一个实操提醒:TURN 的带宽成本要提前算。中继流量是实打实的服务器出口带宽,如果 25% 的用户走 TURN,每人 2Mbps,1000 人同时在线就是 500 Mbps——这在云上是一笔不小的钱。上线前把这个数字算给客户听,避免月底账单意外。

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部