跳到主要内容

一次通话的完整时序

把"建立一路 1v1 视频通话"拆成 7 个阶段,每个阶段都说清楚谁触发、传什么、失败时是什么样。掌握这一张图,后续所有问题都能定位到具体阶段。

1. 全景时序图

2. 阶段 1:媒体采集

触发:用户点"加入会议"。

关键 API

const stream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: true, noiseSuppression: true },
video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: 30 },
});

易错点

  • 非 HTTPS(含 localhost 之外)直接被浏览器拒绝。
  • iOS Safari 必须由用户手势触发,不能在 load 自动调用。
  • constraints 不满足时浏览器会降级而不是报错,必须用 track.getSettings() 复核真实参数。

3. 阶段 2:连接建立

应用自定义的房间模型,常见的最简模板:

const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:turn.example.com:3478', username: 'u', credential: 'p' },
],
iceTransportPolicy: 'all', // 'relay' 强制走 TURN,仅排障用
bundlePolicy: 'max-bundle', // 推荐
rtcpMuxPolicy: 'require', // 推荐
});
stream.getTracks().forEach(t => pc.addTrack(t, stream));

易错点

  • iceServers 只配 STUN 没配 TURN:现网 NAT 穿透成功率掉到 80%。
  • 不设置 bundlePolicy: 'max-bundle':每条 m-line 单独建连,端口数翻倍。
  • addTrack 之前调用 createOffer:得到的 SDP 没有 m-line。

4. 阶段 3:协商(Offer/Answer)

JSEP 状态机的最小子集:

stable
├─ setLocalDescription(offer) → have-local-offer
│ └─ setRemoteDescription(answer) → stable
└─ setRemoteDescription(offer) → have-remote-offer
└─ setLocalDescription(answer) → stable

典型代码(A 是发起方):

const offer = await pcA.createOffer();
await pcA.setLocalDescription(offer);
sendSignal({ type: 'offer', sdp: offer.sdp });

// B 收到 offer
await pcB.setRemoteDescription({ type: 'offer', sdp });
const answer = await pcB.createAnswer();
await pcB.setLocalDescription(answer);
sendSignal({ type: 'answer', sdp: answer.sdp });

// A 收到 answer
await pcA.setRemoteDescription({ type: 'answer', sdp });

glare(双方同时 offer):用 Perfect Negotiation 模式解决,分配一个 polite 角色让步。详见 03-信令与NAT穿透/Perfect-Negotiation模式.md

5. 阶段 4:候选交换(Trickle ICE)

候选不是一次性给出的,浏览器边收集边触发 icecandidate

pc.addEventListener('icecandidate', ({ candidate }) => {
if (candidate) sendSignal({ type: 'candidate', candidate });
// candidate === null 表示收集结束
});

// 收到对端候选
pc.addIceCandidate(remoteCandidate);

候选类型

type来源说明
host本机 IP同一局域网可直连
srflxSTUN 返回公网映射,最常用
prflx对端发现连通性检查中产生
relayTURN 中继兜底,所有 NAT 穿透失败时用

Chrome 默认隐藏本地 IP,host 候选会变成 *.local mDNS 域名。服务器端 SFU 解析不了 mDNS,必须有 TURN 兜底。

6. 阶段 5:连通性检查

ICE Agent 把所有 (本地候选, 远端候选) 配对成 candidate pair,按优先级发送 STUN Binding 请求。第一个双向通过的 pair 被选中(nominated)。

调试入口chrome://webrtc-internals → 选中 PeerConnection → 看 selected-candidate-pair 的变化。

7. 阶段 6:DTLS 握手

ICE 选定 pair 后,立刻在这条路径上跑 DTLS 1.2 握手。每端有一个临时证书,指纹(fingerprint)已经在 SDP 里交换过,互相校验后从 DTLS master secret 派生 SRTP 密钥。

SDP a-line: a=fingerprint:sha-256 4A:AD:B9:B1:...
DTLS 握手实际验证证书是否匹配此指纹

失败现象pc.connectionState 卡在 connecting 然后 failedwebrtc-internals 显示 DTLS error。常见原因:客户端时间不同步、证书过期、TURN 中继丢包导致握手包超时。

8. 阶段 7:媒体传输

握手完成后:

  • 音视频走 SRTP,每个 RTP 包加密 + 鉴权。
  • DataChannel 走 SCTP-over-DTLS,提供可选的可靠/有序语义。
  • 控制信息走 SRTCP,包括 NACK、PLI、REMB、TWCC 反馈。

pc.connectionState === 'connected' 即代表已可用。

9. 状态机速查

状态含义排查
iceGatheringState候选收集进度complete 表示本地候选收齐
iceConnectionStateICE 连通性checking → connected → completed
connectionState整体连接(ICE + DTLS)看这个就够了
signalingStateSDP 协商进度卡在 have-local-offer 通常是信令链路断了

10. 常见失败位置 Top 5

  1. 阶段 1:HTTPS 缺失 → getUserMedia 直接拒绝。
  2. 阶段 3addTrackcreateOffer 顺序错 → SDP 没 m-line。
  3. 阶段 4:忘了交换 ICE 候选 → 永远 iceConnectionState: checking
  4. 阶段 5:没部署 TURN,对称 NAT 用户 → ICE 失败。
  5. 阶段 6:客户端时间偏差 > 5min → DTLS 握手失败。

11. 权威资料