3. WebRTC架构与媒体流:PeerConnection、SDP、ICE 与 MediaStream/Track 处理流程

WebRTC 这玩意儿,说白了就是浏览器里的实时通信引擎。很多朋友一上来就调 API,结果连 PeerConnection 的状态机都没搞明白,遇到问题就抓瞎。我个人习惯是先把骨架搭清楚,再填血肉。

这一章我们聚焦两个核心:信令与连接建立(SDP/ICE/PeerConnection)和媒体流处理(MediaStream/Track)。这两块是 WebRTC 的任督二脉,打通了,后面做 H265 编码集成才顺。

3.1 PeerConnection:WebRTC 的“连接器”

RTCPeerConnection 是整个 WebRTC 的枢纽。它负责管理编码器、网络传输、状态转换。你可以把它想象成一个“连接器”——一端接本地媒体,另一端接远端媒体,中间走的是 ICE 打通的 P2P 通道。

核心要点:PeerConnection 不是简单的 socket,它是一个状态机。从 "new" 到 "connecting" 再到 "connected",每一步都有回调。我见过太多人忽略 oniceconnectionstatechange 事件,结果网络切换时直接断连。

3.1.1 状态机与事件

PeerConnection 的状态变化主要分两类:ICE 连接状态和信令状态。ICE 状态告诉你网络通不通,信令状态告诉你 SDP 交换到哪一步了。

状态 含义 常见触发场景
new 刚创建,还没开始连接 new RTCPeerConnection()
checking 正在收集候选者、做连通性检测 setLocalDescription 后
connected 至少一条候选路径通了 STUN/TURN 协商成功
disconnected 连接丢了,但可能恢复 网络抖动、NAT 绑定超时
failed 彻底挂了,需要重建 ICE 超时、TURN 失效

嗯,这里要注意:disconnected 和 failed 的区别。前者是“暂时失联”,系统还会尝试恢复;后者是“没救了”,必须重建 PeerConnection。我在项目中遇到过一个问题——移动端切 4G 到 WiFi 时,ICE 状态先变 disconnected,然后自动恢复。如果这时候直接销毁连接,就过度设计了。

3.2 SDP:媒体能力的“谈判桌”

SDP(Session Description Protocol)不是传输协议,它是描述媒体能力的文本。WebRTC 用它来交换双方支持的编码格式、分辨率、带宽、加密参数等。

一个典型的 SDP 长这样:

v=0
o=- 123456 2 IN IP4 0.0.0.0
s=-
t=0 0
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98
a=rtpmap:96 H264/90000
a=rtpmap:97 H265/90000
a=rtpmap:98 VP8/90000
a=fmtp:96 profile-level-id=42e01f;packetization-mode=1
a=fmtp:97 profile-space=0;tier-flag=0;profile-id=1
a=mid:video
a=sendrecv

你看,m=video 这一行声明了视频媒体,后面跟着 payload type 列表。每个 payload type 对应一个编码格式。WebRTC 的协商逻辑是:双方取交集。你支持的编码越多,兼容性越好,但协商时间也越长。

避坑指南:我曾经在集成 H265 时,SDP 里写了 a=rtpmap:97 H265/90000,但 Chrome 不认。为什么?因为 Chrome 默认的 H265 支持需要硬件解码,而且 payload type 不能和 H264 冲突。后来我改成动态 payload type 范围(96-127),并在 offer 里把 H265 排在 H264 前面,才搞定。

3.2.1 Offer/Answer 模型

SDP 交换遵循 Offer/Answer 模型。发起方创建 offer,接收方回复 answer。这个过程必须通过信令服务器中转——WebRTC 不负责信令传输,只负责生成和解析 SDP。

  1. 创建 Offer:调用 createOffer(),浏览器根据本地媒体流生成 SDP。
  2. 设置本地描述:setLocalDescription(offer),PeerConnection 开始收集 ICE 候选者。
  3. 发送到远端:通过信令通道(WebSocket/Socket.io)把 offer 发给对方。
  4. 远端设置:对方调用 setRemoteDescription(offer),然后创建 answer。
  5. 交换完成:双方都设置完 local/remote description,开始 ICE 连接。

你想想看,如果信令服务器延迟高,或者丢包,SDP 交换就会卡住。所以生产环境一定要做信令重试和超时处理。

3.3 ICE:穿透 NAT 的“探路者”

ICE(Interactive Connectivity Establishment)是 WebRTC 最复杂的部分。它的任务是:在复杂的网络环境下,找到一条能用的 P2P 路径。

ICE 会收集三类候选者(candidate):

  • host:本机 IP,局域网内直连。
  • srflx:通过 STUN 服务器反射出来的公网 IP。
  • relay:通过 TURN 服务器中转,兜底方案。

ICE 的优先级是:host > srflx > relay。能直连绝不中转,因为 TURN 服务器带宽很贵。

注意:ICE 不是一次性的。网络变化时(比如从 WiFi 切到 4G),ICE 会重新触发候选者收集和连通性检测。如果你没有监听 onicecandidate 事件并重新发送候选者,连接就会断。

3.4 媒体流处理:MediaStream 与 MediaStreamTrack

聊完连接,我们看媒体。WebRTC 的媒体模型分两层:MediaStream(流)和 MediaStreamTrack(轨)。

一个 MediaStream 可以包含多个 Track,比如一个视频 Track 和一个音频 Track。每个 Track 是独立的,可以单独控制开关、音量、编码参数。

3.4.1 获取本地媒体

用 getUserMedia() 获取摄像头和麦克风:

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

// 获取视频轨
const videoTrack = stream.getVideoTracks()[0];
// 获取音频轨
const audioTrack = stream.getAudioTracks()[0];

这里有个坑:getUserMedia 在移动端 Safari 上必须由用户手势触发,否则返回 rejected。我刚开始做移动端适配时,在页面加载时直接调用,结果 iOS 上一直拿不到权限。后来改成点击按钮后再调用,问题解决。

3.4.2 添加 Track 到 PeerConnection

拿到 Track 后,通过 addTrack() 添加到 PeerConnection:

// 每个 Track 单独添加
pc.addTrack(videoTrack, stream);
pc.addTrack(audioTrack, stream);

// 或者用 addTransceiver 更灵活
const transceiver = pc.addTransceiver('video', {
  direction: 'sendrecv',
  streams: [stream]
});

addTransceiver 比 addTrack 更强大。你可以指定 direction(sendonly/recvonly/sendrecv/inactive),还可以绑定编码参数。我在做 H265 编码集成时,就用 addTransceiver 的 sendEncodings 参数来控制 simulcast 的码率分层。

3.4.3 远端媒体渲染

远端媒体通过 ontrack 事件接收:

pc.ontrack = (event) => {
  // event.streams 是远端 MediaStream 数组
  // event.track 是具体的 Track
  const remoteVideo = document.getElementById('remoteVideo');
  remoteVideo.srcObject = event.streams[0];
};

嗯,这里要注意:ontrack 可能触发多次,比如对方中途开启了屏幕共享。你需要维护一个 Track 列表,而不是每次都替换 srcObject。

3.5 整体架构图

下面这张图把上面讲的内容串起来。从本地媒体采集,到 PeerConnection 的 SDP/ICE 协商,再到远端渲染,一目了然。

WebRTC 媒体流与连接建立流程 本地 MediaStream VideoTrack + AudioTrack addTrack / addTransceiver RTCPeerConnection SDP Offer/Answer 协商 ICE 候选者收集与连通性检测 P2P 数据传输 (SRTP/SCTP) 远端 MediaStream ontrack 事件接收 渲染到 <video> 信令服务器 (WebSocket) 关键说明: 1. 本地 MediaStream 通过 addTrack/addTransceiver 绑定到 PeerConnection 2. SDP 协商和 ICE 候选者交换通过信令服务器中转 3. 连通性检测通过后,媒体数据走 P2P 通道(或 TURN 中转) 4. 远端通过 ontrack 事件接收 Track,渲染到 video 元素

3.6 避坑总结

最后,我把这些年踩过的坑列一下,你写代码时多留个心眼:

  • SDP 协商超时:信令服务器如果延迟超过 10 秒,ICE 可能已经超时。建议在 offer/answer 交换时加一个 15 秒的定时器,超时后重建 PeerConnection。
  • ICE 候选者丢失:有些信令实现只发一次候选者,但 ICE 是持续收集的。一定要监听 onicecandidate 事件,每次有新候选者就发出去。
  • Track 生命周期:远端关闭摄像头时,ontrack 不会触发移除事件。你需要监听 onremovetrack 来清理 UI。
  • 编码协商失败:如果对方不支持 H265,你的 offer 里写了 H265 但 answer 里没有,浏览器会自动降级到 H264。但如果你强制只写 H265,连接会失败。
我的习惯:在 PeerConnection 创建时,先设置一个 oniceconnectionstatechange 回调,打印所有状态变化。调试阶段这个日志能帮你快速定位是网络问题还是编码问题。