2、WebRTC基础回顾:整体架构、核心API与信令流程

好,咱们正式开始第二讲。在深入 H265 与 WebRTC 的融合之前,我强烈建议先把 WebRTC 的底子夯实。说白了,H265 只是视频编码这块的“零件”,你得先搞懂 WebRTC 这辆“车”是怎么跑的,才能知道零件该往哪儿装。

我个人习惯把 WebRTC 看作一个“浏览器原生支持的实时通信黑盒”。你不需要懂底层 UDP 怎么丢包重传,也不需要管音视频怎么同步,浏览器都帮你封装好了。但作为架构师,你必须知道这个黑盒的边界在哪里,以及怎么跟它打交道。

2.1 整体架构:三层模型

WebRTC 的架构,我习惯把它拆成三层来看。这样理解起来最清晰。

核心观点: WebRTC 不是单一协议,而是一套协议栈 + API 的集合。它的设计哲学是“尽可能在浏览器端完成复杂工作,信令层只做轻量协调”。

WebRTC 整体架构(三层模型) 应用层(Application Layer) 你的业务代码:信令逻辑、房间管理、UI交互 核心 API:getUserMedia, RTCPeerConnection, RTCDataChannel 会话层(Session Layer) SDP 协商、ICE 候选收集、DTLS 握手 这一层由浏览器内部实现,开发者通过 API 触发 传输层(Transport Layer) SRTP/SCTP、ICE、STUN/TURN、DTLS 负责 P2P 连接、NAT 穿透、数据加密与传输 💡 开发者主要工作在应用层,但理解会话层和传输层才能解决疑难杂症

嗯,这里要注意。很多新手以为 WebRTC 就是调几个 API 完事。其实不然。你想想看,如果不懂 ICE 和 STUN,线上出现“连不上”的问题,你连日志都看不懂。

2.2 三大核心 API

WebRTC 给开发者暴露了三个核心 API。我一个个说。

2.2.1 getUserMedia:获取音视频流

这是最直观的 API。说白了就是打开摄像头和麦克风。

// 获取摄像头视频(前置摄像头)
const constraints = {
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 },
    facingMode: 'user'
  },
  audio: {
    echoCancellation: true,
    noiseSuppression: true
  }
};

try {
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
  // 将 stream 绑定到 <video> 标签
  document.getElementById('localVideo').srcObject = stream;
} catch (err) {
  console.error('获取媒体失败:', err.name, err.message);
}

避坑指南: 我曾经在移动端项目里遇到过,部分 Android 浏览器在切换前后摄像头时,直接调用 getUserMedia 会闪退。后来我改用 MediaStreamTrack.applyConstraints() 动态切换,才稳定下来。建议你优先用 track 级别的控制,而不是每次都重新获取流。

2.2.2 RTCPeerConnection:核心连接对象

这是 WebRTC 的灵魂。它负责建立和维护点对点连接。

const config = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    {
      urls: 'turn:your-turn-server.com:3478',
      username: 'user',
      credential: 'pass'
    }
  ]
};

const pc = new RTCPeerConnection(config);

// 添加本地流
localStream.getTracks().forEach(track => {
  pc.addTrack(track, localStream);
});

// 监听 ICE 候选
pc.onicecandidate = (event) => {
  if (event.candidate) {
    // 通过信令通道发送给对方
    sendSignalingMessage({
      type: 'candidate',
      candidate: event.candidate
    });
  }
};

// 监听远程流
pc.ontrack = (event) => {
  document.getElementById('remoteVideo').srcObject = event.streams[0];
};

我个人习惯把 RTCPeerConnection 看作一个“状态机”。它内部维护了连接状态、ICE 状态、SDP 状态。调试时多打印 pc.connectionState 和 pc.iceConnectionState,能帮你快速定位问题。

2.2.3 RTCDataChannel:非媒体数据传输

很多人不知道,WebRTC 不光能传音视频,还能传任意二进制数据。而且走的是 SCTP 协议,支持可靠或不可靠传输。

// 创建数据通道(通常在发起方创建)
const dataChannel = pc.createDataChannel('chat', {
  ordered: true,        // 保证顺序
  maxRetransmits: 3     // 最大重传次数
});

dataChannel.onopen = () => {
  console.log('数据通道已打开');
  dataChannel.send('Hello from peer!');
};

dataChannel.onmessage = (event) => {
  console.log('收到消息:', event.data);
};

// 接收方监听
pc.ondatachannel = (event) => {
  const receiveChannel = event.channel;
  receiveChannel.onmessage = (e) => {
    console.log('收到:', e.data);
  };
};

注意: 数据通道的创建时机很关键。必须在 createOffer 之前创建,否则 SDP 中不会包含数据通道的 m-line,导致连接建立后无法使用。我曾经因为这个坑排查了整整一个下午。

2.3 信令流程:连接建立的“握手”

WebRTC 本身不定义信令协议。信令怎么传,完全由你决定——WebSocket、HTTP、甚至飞鸽传书都行。但信令交换的内容是固定的。

标准的信令流程,我总结为四步:

  1. 创建 Offer:发起方调用 pc.createOffer(),生成 SDP(会话描述协议)。
  2. 设置本地描述:发起方调用 pc.setLocalDescription(offer)。
  3. 交换 SDP:通过信令通道将 Offer 发送给接收方。接收方调用 pc.setRemoteDescription(offer)。
  4. 创建并交换 Answer:接收方调用 pc.createAnswer(),设置本地描述,再通过信令通道发回给发起方。

与此同时,ICE 候选也在并行交换。流程表如下:

步骤 发起方(Caller) 信令通道 接收方(Callee)
1 createOffer() → setLocalDescription() 发送 Offer SDP → setRemoteDescription()
2 收集 ICE 候选 发送候选 → 添加 ICE 候选
3 ← 接收 Answer SDP ← 发送 Answer SDP createAnswer() → setLocalDescription()
4 ← 接收 ICE 候选 ← 发送候选 收集 ICE 候选

关键点: Offer/Answer 交换必须严格遵循“先 SDP 后候选”的顺序吗?不一定。实际上 ICE 候选可以在 SDP 交换之前就开始收集,但必须等远程描述设置后才能添加。我建议你让 SDP 交换先完成,再批量添加候选,逻辑更清晰。

2.4 信令状态机与连接建立

为了让你更直观地理解整个流程,我画了一张信令状态机图。它展示了从创建 PeerConnection 到连接建立的全过程。

WebRTC 信令状态机与连接建立流程 创建 PC createOffer() setLocalDescription 等待远程 Answer setRemoteDescription 连接建立成功 📌 信令流程关键说明 1. ICE 候选收集在 createOffer 之后自动开始,与 SDP 交换并行。 2. 必须等 setRemoteDescription 完成后,才能调用 addIceCandidate()。 3. 连接状态变化可通过 pc.onconnectionstatechange 监听。 4. 如果使用 TURN 中继,连接建立时间会比 P2P 慢 200-500ms。

2.5 避坑与经验总结

最后,我分享几个实战中遇到的坑,希望能帮你少走弯路。

  • ICE 候选不完整:我曾经在部署 TURN 服务器时,忘了配置 turns(TLS 加密的 TURN),导致部分企业网络环境下连接失败。建议你同时配置 turn 和 turns。
  • SDP 协商失败:H264 和 VP8 的编码参数差异可能导致协商失败。我习惯在 SDP 中手动指定编码优先级,确保兼容性。
  • 数据通道延迟:如果数据通道用于实时控制信令(比如鼠标位置同步),建议设置 ordered: false 和 maxRetransmits: 0,牺牲可靠性换取低延迟。

小技巧: 调试时,我经常在浏览器控制台打印 pc.localDescription.sdp 和 pc.remoteDescription.sdp,对比双方的 SDP 是否匹配。很多连接问题,看一眼 SDP 就能发现端倪。

好了,WebRTC 的基础就回顾到这里。这些内容虽然基础,但都是后面融合 H265 的基石。你想想看,如果连 SDP 协商和 ICE 流程都不熟,后面怎么在 SDP 里插入 H265 的 fmtp 参数?怎么保证 H265 流能顺利穿透 NAT?