Skip to content

WebRTC 入门到上手:从 1v1 通话到低延迟直播

更新: 5/30/2026 字数: 0 字 时长: 0 分钟

适合读者:要做互动直播、连麦、云游戏、远程协作的团队。本文不堆 RFC,只讲工程师上手要知道的那些事。

image.png

一、WebRTC 是什么,不是什么

一句话:WebRTC 是浏览器/移动端自带的一套"实时音视频 + 数据通道"能力,端到端延迟 200ms–800ms,全平台原生支持。

几个常见误解先拆掉:

  • 不是"点对点"一定不经服务器:浏览器 P2P 只解决"媒体传输"这一段,信令仍然必须走服务器
  • 不是"开箱即用的直播方案":1v1 通话很爽,规模化到万人直播时,WebRTC 的拓扑成本会劝退大多数团队——需要 SFU。
  • 不是"零 CDN":规模化 WebRTC 直播依然要部署边缘节点(SFU 集群),本质上是"另一种 CDN"。
  • 不是"只能传音视频"RTCDataChannel 可以传任意二进制,云游戏、白板协作、文件传输都在用。

二、三条链路:信令、NAT 穿透、媒体传输

WebRTC 建联要打通三件事,缺一不可

┌──────────┐         信令服务器         ┌──────────┐
│  Peer A  │ ◄──── SDP / ICE Offer ───► │  Peer B  │
│          │                            │          │
│          │ ──── STUN/TURN 探测 ─────► │          │
│          │ ◄── 直连 / 中继  (RTP) ──► │          │
└──────────┘                            └──────────┘

2.1 信令(Signaling)

WebRTC 不规定信令协议——这是刻意留的口子。你可以用 WebSocket、Socket.IO、MQTT、甚至裸 HTTP 轮询。信令只干三件事:

  1. 交换 SDP(Session Description Protocol)——谁用什么编码、端口、能力;
  2. 交换 ICE candidate——每个端可能的网络地址;
  3. 控制信令——加入/离开房间、静音、举手等业务语义。

最小经典流程(Offer/Answer):

A: createOffer → setLocalDescription → 发给 B
B: setRemoteDescription → createAnswer → setLocalDescription → 发回 A
A: setRemoteDescription
双方持续交换 ICE candidate(Trickle ICE)

工程要点:

  • Trickle ICE 默认开启:候选地址是"边收集边发"的,不要等全部收齐才发送 SDP,否则建联慢 2–3 秒。
  • 信令要幂等:掉线重连、ICE restart 时会重发 SDP,服务端要能识别"这是同一路会话"。
  • 房间模型 vs 点对点模型:直播必做房间抽象;1v1 通话可以更轻量。

2.2 STUN / TURN / ICE

NAT 穿透是 WebRTC 的死穴。三个角色:

组件做什么带宽成本
STUN告诉客户端"你在公网看起来是什么地址"极低(几字节)
TURNSTUN 穿透不过去时,帮你中继所有媒体流量高(全量转发)
ICE一套算法,把所有候选地址打分、尝试、选出最优路径逻辑层

ICE 候选类型(Network 面板里 iceConnectionState 常见的词):

  • host:本机网卡直连地址;
  • srflx(server reflexive):STUN 探测出来的公网映射;
  • prflx(peer reflexive):对端发来的未知候选;
  • relay:TURN 中继地址。

实战结论

  • STUN 足够便宜,TURN 必须自建或买。公开免费 STUN(Google stun:stun.l.google.com:19302)可以用于 demo,生产必须自建。
  • TURN 命中率通常 10%–30%:企业网、对称 NAT、运营商级 NAT 穿不过去的流量全走 TURN。带宽成本按 上下行翻倍 算。
  • TURN 部署要靠近用户:TURN 服务器在新加坡,用户在美西,延迟直接翻倍。多区域部署是必修课。
  • iceTransportPolicy: 'relay' 强制走 TURN,调试 NAT 问题时超好用:
js
new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.example.com:3478' },
    {
      urls: 'turn:turn.example.com:3478?transport=udp',
      username: 'user', credential: 'pwd',
    },
  ],
  iceTransportPolicy: 'all',  // 调试时改 'relay'
  bundlePolicy: 'max-bundle',  // 强烈推荐,省端口省握手
})

2.3 媒体传输

建联成功后,音视频走 SRTP(加密 RTP)over UDP;数据走 SCTP over DTLS。这些你基本不用动,浏览器帮你处理了 99% 的复杂度——这也是 WebRTC 相比自研 UDP 方案最大的价值。

三、三大 API:这就是全部

3.1 getUserMedia:拿摄像头和麦克风

js
const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 },
    frameRate: { ideal: 30, max: 30 },
    facingMode: 'user',  // 前置 / 'environment' 后置
  },
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true,
  },
})
document.querySelector('video').srcObject = stream

坑:

  • 必须 HTTPS(或 localhost)。HTTP 页面直接 reject。
  • iOS 必须在用户手势里调用:否则权限弹窗直接跳过,返回 NotAllowedError。
  • 摄像头只能被一个 getUserMedia 占用:同一页面多次调用也会冲突,要统一管理流。
  • MediaTrackConstraints 是"建议"不是"强制":给的分辨率和帧率设备可能不支持,会返回最接近的。用 track.getSettings() 看实际值。

3.2 getDisplayMedia:屏幕共享

js
const screen = await navigator.mediaDevices.getDisplayMedia({
  video: { frameRate: 30 },
  audio: true,  // 系统音频,各浏览器支持差异大
})

坑:

  • 必须用户手势触发
  • Chrome 支持 Tab/Window/Screen 三种粒度,Safari 只给整个屏幕;
  • 系统音频采集:Windows Chrome 支持,macOS Chrome 不行(只能采 Tab 音频),Safari 不支持;
  • 共享结束事件track.onended 监听用户主动停止。

3.3 RTCPeerConnection:核心中的核心

js
const pc = new RTCPeerConnection(config)

// 加流
stream.getTracks().forEach(t => pc.addTrack(t, stream))

// 协商
pc.onicecandidate = e => e.candidate && signaling.send('ice', e.candidate)
pc.ontrack = e => remoteVideo.srcObject = e.streams[0]
pc.oniceconnectionstatechange = () => console.log(pc.iceConnectionState)

const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
signaling.send('offer', offer)

几个一定要记住的:

  • addTrack 不是 addStream:老 API addStream 已废弃;
  • RTCRtpTransceiver 是现代玩法pc.addTransceiver('video', { direction: 'sendrecv' }) 可以在没 track 时先占坑,SFU 场景很常用;
  • getStats() 是排查神器:返回实时 RTT、丢包、码率、Jitter Buffer 长度等,必须接入监控;
  • restartIce() 用于网络切换:Wi-Fi→4G 时主动触发 ICE restart 重建路径,比等超时快 10 秒。

四、SFU vs MCU:规模化直播的架构选型

1v1 通话可以纯 P2P。一旦超过 3 个人,必须引入中心节点,就有了两种主流架构。

4.1 Mesh(纯 P2P,仅 1v1 或 ≤4 人可用)

每个端都要给所有其他端推一路流
N 人 = N × (N-1) 路上行 + 下行

4 人就要每端上行 3 路,带宽爆炸。只有视频会议 demo 还在用。

4.2 MCU(Multipoint Control Unit,混流器)

所有端 → MCU → 解码 → 混流合成一路 → 编码 → 下发给所有端
优点缺点
客户端下行只有 1 路,省带宽服务器要解码+编码,CPU/GPU 成本极高
画面布局固定,移动端友好不灵活,用户无法自定义布局
兼容传统 SIP/PSTN 最好多加 100–300ms 延迟

4.3 SFU(Selective Forwarding Unit,选择性转发)

所有端 → SFU → 不解码,只按需转发 RTP 包给订阅者
优点缺点
服务器不转码,CPU 成本低 1–2 个数量级客户端下行要订阅 N-1 路
支持 Simulcast/SVC 按需选层,端侧自适应带宽省在服务端,但客户端压力大
延迟最低(只转发不重编码)布局由客户端自己合成

业内共识:规模化 WebRTC 直播/会议全部走 SFU。Jitsi、mediasoup、Janus、LiveKit、声网、Zoom 都是 SFU 为核。

4.4 SFU 的两个关键优化

  • Simulcast(联播):推流端同时推 1080p/720p/360p 三路,SFU 根据订阅者网络情况选一路下发。上行成本换下行灵活性,是规模化互动直播的标配。
  • SVC(Scalable Video Coding):单路分层编码(时域/空域/质量),SFU 只转发部分层。比 Simulcast 更省上行,但编码器支持差(VP9/AV1 支持,H.264 SVC 生态弱)。

4.5 混合架构:小规模 SFU + 大规模 CDN

真正的"百万互动直播"架构往往是:

主播 + 连麦嘉宾  ─►  SFU(低延迟 WebRTC 互动路)

                      ▼ 混流/转封装
                    RTMP/HLS/FLV  ─►  CDN  ─►  百万观众

核心:互动路上 SFU,观看路上 CDN。连麦的 10 个人走 WebRTC,看直播的 100 万人走 LL-HLS/FLV。成本与延迟的最优平衡点。

五、WHIP / WHEP:终于有个标准了

WebRTC 没有标准信令,历史上每家都造轮子。2022 年开始落地的 WHIP(WebRTC-HTTP Ingestion Protocol)和 WHEP(WebRTC-HTTP Egress Protocol)解决了这个问题。

5.1 WHIP:推流标准(主播 → 服务器)

就是一次 HTTP POST 把 SDP Offer 发过去,服务器回 Answer。之后媒体走 WebRTC。

POST /whip/endpoint HTTP/1.1
Content-Type: application/sdp
Authorization: Bearer <token>

v=0
o=- ...
m=video ...
...

HTTP/1.1 201 Created
Location: /whip/resource/abc123
Content-Type: application/sdp

v=0
o=- ...

之后用 PATCH /whip/resource/abc123 更新 ICE、DELETE 结束推流。就这么简单。

5.2 WHEP:拉流标准(服务器 → 观众)

和 WHIP 对称,POST /whep/endpoint 拿到媒体。

5.3 为什么这事重要

  • 推拉流终于可以换厂商了:以前每家 SFU 的推流 SDK 都不一样,换云厂商等于重写;现在 WHIP 标准化了。
  • OBS / FFmpeg / xgplayer 都在接入 WHIP/WHEP:OBS 30+ 原生支持 WHIP 推流,替代 RTMP 只差时间。
  • 前端代码可以很短
js
// WHEP 拉流(极简版)
async function playWhep(whepUrl, video) {
  const pc = new RTCPeerConnection({ iceServers: [...] })
  pc.addTransceiver('video', { direction: 'recvonly' })
  pc.addTransceiver('audio', { direction: 'recvonly' })
  pc.ontrack = e => video.srcObject = e.streams[0]

  const offer = await pc.createOffer()
  await pc.setLocalDescription(offer)

  const res = await fetch(whepUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/sdp' },
    body: offer.sdp,
  })
  const answer = await res.text()
  await pc.setRemoteDescription({ type: 'answer', sdp: answer })

  // res.headers.get('location') 拿到资源 URL,后续用于 DELETE
}

对比传统 RTMP 推流:RTMP 是 TCP,抖动大时排队爆炸;WHIP 是 UDP 底层,拥塞控制现代得多。RTMP 被 WHIP 取代只是时间问题

六、前端六大常见坑

6.1 NAT 穿透失败率

症状:iceConnectionState 卡在 checking,最终 failed

  • 原因:对称 NAT(Symmetric NAT)、运营商级 NAT(CGNAT)、企业防火墙。
  • 定位:打开 chrome://webrtc-internals/,看 ICE candidate pair 的状态,能一眼看出是走到 TURN 还是直接 failed。
  • 解法
    1. TURN 必配且必须 TCP+UDP 双协议turn:host:3478?transport=tcp 兜底企业防火墙只允许 TCP 443 出口的情况;
    2. TURN over TLSturns:):443 端口 + TLS,穿透最硬的企业防火墙;
    3. 多 TURN 服务器并行尝试iceServers 数组里放多个。

6.2 丢包与抗弱网

WebRTC 自带一套抗丢包机制,了解下就知道它在哪儿:

  • NACK:接收端发现包丢了,请求重传。适合低延迟场景。
  • FEC(Forward Error Correction):发送端主动加冗余,接收端直接恢复。适合丢包率高的网络。
  • PLI / FIR:关键帧请求。接收端解码失败时主动请求一个 I 帧。
  • Jitter Buffer:接收端的缓冲区,平滑网络抖动。太小易卡顿,太大延迟高。

前端能干预的:

  • RTCRtpSender.setParameters() 动态调整码率、分辨率;
  • encodings[].priority 指定多路流的优先级;
  • 监听 getStats() 里的 packetsLostjitterframesDropped,做体验上报。

6.3 回音消除(AEC)

浏览器的 AEC 默认开启(echoCancellation: true),但这些场景会失效:

  • 外接声卡、蓝牙耳机延迟大:AEC 的参考信号对不齐,回音漏出来;
  • 通过 Web Audio API 处理过音频getUserMedia 拿到的 track 一旦过 AudioContext 路由再回 PeerConnection,AEC 的参考链路就断了;
  • 多人同屏时系统声音被采:共享屏幕带系统音频,对端的声音被采回去形成回音。

解法:

  • 能用 getUserMedia 的 AEC 就不要自己接 WebAudio;
  • 必须接 WebAudio 时,用 MediaStreamTrackProcessor + WebCodecs 路径,AEC 自己做或用 RNNoise 一类 wasm 方案;
  • 提醒用户戴耳机是最便宜的方案。

6.4 带宽自适应:GCC

WebRTC 的拥塞控制算法叫 GCC(Google Congestion Control),基于延迟梯度+丢包的混合模型。它做两件事:

  1. 探测可用带宽(BWE, Bandwidth Estimation);
  2. 指导编码器调整码率/分辨率/帧率

工程上你一般不用碰 GCC 内部,但要理解它:

  • 上行码率不是你设多少就多少:GCC 会根据网络压回去;
  • Simulcast 三路的 BWE 是独立的:上行弱的端会被降级到只推 360p;
  • 自研应用层逻辑不要和 GCC 打架:比如你主动把分辨率拉到 1080p 但 GCC 判定带宽不够,会频繁切换,体验更差。

监控关键指标:getStats() 里的 availableOutgoingBitratequalityLimitationReasonbandwidth / cpu / none)。

6.5 音视频同步

WebRTC 帮你做了 RTP 时间戳同步,但有两个场景翻车:

  • Canvas 合成画面 + 麦克风音频canvas.captureStream() 的时间戳可能和音频对不上;
  • WebCodecs 自研链路:如果你用 WebCodecs 解完自己渲染,同步逻辑要自己写。

6.6 移动端特有坑

  • iOS Safari 的后台策略:App 进后台 WebRTC 连接会被暂停,音频可能继续但视频停止。PWA 模式能缓解。
  • Android WebView 的 echo:小米、OPPO 某些型号 AEC 实现有 bug,建议手动降级音频参数或提示用户戴耳机。
  • 省电策略:移动端低电量模式会降低 WebRTC 的最大码率和帧率,表现为"一到 20% 电量画面就糊"。
  • H.264 vs VP8:iOS 只硬解 H.264,Android 看厂商。跨平台推流 codec 协商要给 H.264 最高优先级。

七、最小可用 Demo:1v1 视频通话

js
// ===== 公用:信令(这里用 WebSocket 示意)=====
const sig = new WebSocket('wss://sig.example.com/room/123')
sig.onmessage = async ({ data }) => {
  const msg = JSON.parse(data)
  if (msg.type === 'offer') await onOffer(msg)
  else if (msg.type === 'answer') await pc.setRemoteDescription(msg)
  else if (msg.type === 'ice' && msg.candidate) await pc.addIceCandidate(msg.candidate)
}
const send = obj => sig.send(JSON.stringify(obj))

// ===== Peer 初始化 =====
const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.example.com' },
    { urls: 'turn:turn.example.com', username: 'u', credential: 'p' },
  ],
  bundlePolicy: 'max-bundle',
})
pc.onicecandidate = e => e.candidate && send({ type: 'ice', candidate: e.candidate })
pc.ontrack = e => document.querySelector('#remote').srcObject = e.streams[0]
pc.oniceconnectionstatechange = () => {
  if (pc.iceConnectionState === 'failed') pc.restartIce()
}

// ===== 加本地流 =====
const local = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
document.querySelector('#local').srcObject = local
local.getTracks().forEach(t => pc.addTrack(t, local))

// ===== 主叫方发起 Offer =====
async function call() {
  const offer = await pc.createOffer()
  await pc.setLocalDescription(offer)
  send({ type: 'offer', sdp: offer.sdp })
}

// ===== 被叫方处理 Offer =====
async function onOffer(msg) {
  await pc.setRemoteDescription(msg)
  const answer = await pc.createAnswer()
  await pc.setLocalDescription(answer)
  send({ type: 'answer', sdp: answer.sdp })
}

这 30 行就是 1v1 的全部核心。规模化时把 pc 换成针对 SFU 的 PeerConnection 管理即可。

八、学习路径建议

  1. 跑通 1v1:用上面 30 行 demo,加一个最简单的 WebSocket 信令,先让两个浏览器 tab 能通话。
  2. 引入 TURN:自建一个 coturn(开源 TURN 实现),把 iceTransportPolicy: 'relay' 跑通,理解中继的带宽代价。
  3. 读一个开源 SFUmediasoup 代码量适中、架构清晰、文档好,是理解 SFU 最快的路径。
  4. 接入 WHIP/WHEP:用 OBS 推到 MediaMTX(支持 WHIP),浏览器用 WHEP 拉,全链路跑一遍。
  5. 做监控getStats() 每秒采样,上报 RTT、丢包、码率、iceConnectionState。没监控的 WebRTC 项目 = 黑盒,出了问题只能猜。
  6. 进阶话题:Simulcast 接入、SVC(AV1)编码、E2EE(端到端加密 SFrame)、拥塞控制调优。

九、一张速查

问题看哪里
建联失败chrome://webrtc-internals/ → ICE candidate pair state
画面卡顿getStats()framesDropped, jitter, packetsLost
码率上不去qualityLimitationReason = bandwidth / cpu
回音检查 echoCancellation 是否被 WebAudio 旁路
跨网络切换断iceConnectionState = disconnectedrestartIce()
规模化成本爆确认架构是 Mesh 还是 SFU;互动路 + 观看路是否分离
RTMP 迁移WHIP/WHEP
iOS 特殊playsinline、H.264、必须手势

结语

WebRTC 的学习曲线有个典型特征:前 30 行代码 10 分钟跑通,后面的每一个百分点稳定性都要花一个月。真正的门槛从来不在 API,而在:

  • TURN 部署与成本控制
  • SFU 的架构选型与运维
  • 移动端的长尾兼容性
  • 监控体系与故障定位

对大多数团队,最务实的路径是:用成熟的 SFU(mediasoup / LiveKit / 云厂商)打底 + 前端专注业务逻辑 + WHIP/WHEP 做接入层标准化。把精力花在 UX 和监控上,比自己啃 RFC 5245 重写 ICE 有价值得多。

WebRTC 不是银弹,但在"互动性 > 规模"的场景里,它目前没有对手。