WebRTC 实战:构建视频会议与直播

FreeGuideOnline 最新 2026-07-03

用户A 信令服务器 用户B | | | |-- createOffer --------->| | | |-- forward offer ------->| | | |-- setRemoteDesc | |<-- createAnswer ---------| |<-- forward answer -------| | | | | |-- ICE candidate (A) --->|-- forward candidate ---->| |<-- ICE candidate (B) ----|-------------------------| | | |<========== 媒体流 / 数据通道直接传输 =============>|


---

## 第一步:获取本地媒体流

任何 WebRTC 应用都从获取用户的摄像头和麦克风开始。使用 `navigator.mediaDevices.getUserMedia()`。

```javascript
async function startLocalStream() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: {
        width: { ideal: 1280 },
        height: { ideal: 720 },
        facingMode: 'user'
      },
      audio: true
    });
    // 将本地画面插入 video 元素
    document.getElementById('localVideo').srcObject = stream;
    return stream;
  } catch (error) {
    console.error('获取媒体失败:', error);
  }
}

关键要点

  • 必须运行在 httpslocalhost 环境下(安全上下文要求)。
  • 用户需要授予摄像头和麦克风权限。
  • 可以通过 video 约束控制分辨率、帧率、前后摄像头等。

第二步:理解信令与交换 SDP

两个浏览器要建立直连,必须先交换会话描述协议(Session Description Protocol, SDP)信息。SDP 包含媒体格式、编解码器、网络信息等元数据,不包含媒体流本身。

WebRTC 本身没有规定信令传输方式,你需要自己实现信令服务器(常用 WebSocket)。交换过程有两种常见模式:

1. Offer / Answer 模式(一对一通话)

一方创建 Offer,另一方创建 Answer。

// 创建方 (Caller)
const peerConnection = new RTCPeerConnection(configuration);
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
// 通过信令服务器将 offer 发送给对方

// 接收方 (Callee)
await peerConnection.setRemoteDescription(offer);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
// 将 answer 送回发起方

一旦双方都设置了 Local 和 Remote Description,连接就开始协商。

2. Perfect Negotiation 模式(适合会议等复杂场景)

建议所有应用都采用完美协商模式,避免信令状态冲突。详见 WebRTC 官方推荐


第三步:ICE 与 NAT 穿透

建立 P2P 连接面临的最大障碍是网络地址转换(NAT)和防火墙。WebRTC 使用 ICE(Interactive Connectivity Establishment)框架自动寻找最优传输路径。

  • STUN 服务器:帮助获取公网 IP 和端口类型,实现简单穿透。
  • TURN 服务器:当点对点无法连接时,作为中继转发媒体流(消耗服务器带宽)。

你需要提供 ICE 服务器配置:

const configuration = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' }, // 免费 STUN
    {
      urls: 'turn:your-turn-server.com:3478',
      username: 'user',
      credential: 'password'
    }
  ]
};

当 ICE 收集到候选地址时,通过信令通道发送给远端:

peerConnection.onicecandidate = event => {
  if (event.candidate) {
    signaling.send({ type: 'candidate', candidate: event.candidate });
  }
};

对方收到后添加:

await peerConnection.addIceCandidate(candidate);

第四步:构建一对一视频通话

完整的一对一通话代码骨架:

发起端 (index.html 脚本)

const pc = new RTCPeerConnection(iceConfig);
const localStream = await startLocalStream();
localStream.getTracks().forEach(track => pc.addTrack(track, localStream));

// 处理远端流
pc.ontrack = event => {
  remoteVideo.srcObject = event.streams[0];
};

// 创建 Offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send({ type: 'offer', sdp: pc.localDescription });

// 监听来自信令的 Answer
signaling.on('answer', async (answer) => {
  await pc.setRemoteDescription(new RTCSessionDescription(answer));
});

signaling.on('candidate', async (candidate) => {
  await pc.addIceCandidate(candidate);
});

接收端 流程对称,在收到 offer 后创建 answer 并设置。

此时代码已能实现浏览器间的音视频通话。将本地和远端 <video> 元素显示即可。


第五步:使用 DataChannel 实现文字聊天或文件传输

RTCDataChannel 提供低延迟、非可靠或可靠的数据传输,基于 SCTP 协议。非常适合传输文字、控制指令、或小文件。

创建数据通道(任一 Peer 调用均可):

const dataChannel = pc.createDataChannel('chat');
dataChannel.onopen = () => console.log('数据通道已打开');
dataChannel.onmessage = event => {
  console.log('收到消息:', event.data);
};

// 发送文本
dataChannel.send('Hello WebRTC!');

另一端监听:

pc.ondatachannel = event => {
  const receiveChannel = event.channel;
  receiveChannel.onmessage = e => console.log(e.data);
};

第六步:变身为视频会议(多人音视频)

多人会议的核心挑战是拓扑结构选择。常见方案:

1. Mesh 架构(全连接)

每个参会者和其他所有参会者建立 P2P 连接。适合 3~4 人的小房间,无需服务器中转媒体。

       A
     / | \
    B--C--D

优点:服务器简单,零媒体成本。
缺点:上行带宽和编码能力随人数线性增长,移动端难承受。

2. SFU 架构(选择性转发单元)

每个人只上传一路流到 SFU 服务器,SFU 负责转发给其他人。是目前最主流方案。

    A → \            / → B
    B → —[ SFU ] —— → C
    C → /            \ → D

可以配合 Simulcast(多分辨率上传)和 SVC 优化带宽。需集成 Mediasoup、Janus、Pion 等 SFU 库。

3. MCU 架构(多点控制单元)

服务器将多路流解码、合成为单一视频流推给所有客户端。客户端负载极低,但服务器计算成本高。

建议:学习阶段先用 Mesh 了解原理,生产环境迁移到 SFU。

实现简单 Mesh 会议

维护一个 Peer 列表,每当新用户加入时,已有的所有用户和新用户建立连接。伪代码:

// 信令服务器管理房间成员,发送新成员加入事件
signaling.on('new-peer', async (peerId) => {
  const pc = createPeerConnection(peerId);
  // 将本地流加入该连接
  localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
  // 建立 Offer...
});

// 新加入的成员先获取房间内已有成员列表,逐一建立连接
signaling.on('existing-peers', (peerList) => {
  peerList.forEach(peerId => createOfferFor(peerId));
});

这种方式可快速验证多人通话功能。


第七步:构建直播推流

直播场景下,主播上传一路流,观众只接收(单向),无需对等连接。核心思路:

  • 主播使用 WebRTC 推流到媒体服务器(如 SRS、MediaSoup、Millicast)。
  • 观众从媒体服务器拉流(可用 WebRTC 拉,也可转成 HLS/FLV 等低延迟协议)。

最简单实验方案:使用 WebRTC + Mediasoup 搭建推拉流服务。也可以使用现成服务如 Daily、Agora、Twilio 等。

客户端推流代码(主播)

// 与建立普通连接相同,只不过远端是服务器而非其他浏览器
const pc = new RTCPeerConnection();
const stream = await startLocalStream();
stream.getTracks().forEach(track => pc.addTrack(track, stream));
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 将 offer SDP 通过 HTTP/WS 发给你的服务器,获取 Answer
const response = await fetch('/webrtc/publish', {
  method: 'POST',
  body: JSON.stringify({ sdp: pc.localDescription }),
  headers: { 'Content-Type': 'application/json' }
});
const answer = await response.json();
await pc.setRemoteDescription(new RTCSessionDescription(answer.sdp));

观众拉流:从服务器获取 Offer SDP 并应答,然后接收媒体轨道显示。实际服务器会处理好 track 转发。


第八步:常见问题与优化技巧

  1. 权限问题:确保 HTTPS,或者开发时使用 localhost。部分浏览器还要求用户手势后才能调用 getUserMedia。
  2. 回声消除:WebRTC 内置音频处理,但使用外部扬声器时可能产生回声。可设置 videoElement.volume = 0 防止本地播放捕获回声。
  3. 带宽控制:可通过 RTCRtpSender.setParameters() 调整比特率,或使用 Simulcast 发送多路不同质量流。
  4. 连接状态监控:监听 iceConnectionState 变化,及时提示用户重新连接。
  5. 移动端适配:考虑屏幕旋转、前后摄像头切换,使用 facingMode 约束和 orientation 事件。
  6. 调试利器:Chrome 内置 chrome://webrtc-internals,可查看详细连接统计、候选、丢包等。

动手实践:从 Demo 到产品

建议按以下路径进阶练习:

  1. 本地单页测试:不使用信令,手动交换 SDP(两个浏览器标签页)。熟悉 API。
  2. 引入 WebSocket 信令:搭建简单 Node.js 信令服务器,实现一对一双向视频。
  3. 添加 DataChannel:实现文字聊天。
  4. 扩展为 Mesh 小组会议
  5. 集成 SFU:例如使用 Mediasoup 搭建 SFU 服务,替换 Mesh。
  6. 加入身份认证、房间管理、录制等:完善后端。

示例信令服务器(Node.js + WebSocket)

// 精简版示例仅展示思路
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', ws => {
  ws.on('message', data => {
    const msg = JSON.parse(data);
    // 广播给房间内其他成员(需要维护房间状态)
    broadcast(msg, ws);
  });
});