Skip to content

WebRTC

WebRTC(Web Real-Time Communication)是一个支持网页浏览器进行实时语音对话、视频聊天和P2P文件分享的技术标准,由 Google 主导开发,并被 W3C 和 IETF 采纳为标准。

核心概念

1.点对点通信(P2P)

WebRTC 允许浏览器之间直接建立连接,无需中间服务器传输媒体数据(尽管信令过程通常需要服务器)。

2.实时音视频(MediaStream API)

用于捕捉和处理摄像头、麦克风等媒体输入。

js
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  .then(stream => {
    videoElement.srcObject = stream;
  });

3.RTCPeerConnection

核心 API,用于建立 P2P 连接、处理 NAT/ICE/SDP 等复杂流程。

4.RTCDataChannel

用于建立任意数据通道,实现文件传输或游戏同步等功能。

5.信令(Signaling)

WebRTC 不定义信令方式,开发者可使用 WebSocket、HTTP、Socket.io 等传输 SDP 和 ICE 候选信息。

示例代码(简化 P2P 视频通话)

js
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();

pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);

navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(stream => {
  pc1.addTrack(stream.getTracks()[0], stream);
  return pc1.createOffer();
}).then(offer => {
  return pc1.setLocalDescription(offer).then(() => offer);
}).then(offer => {
  return pc2.setRemoteDescription(offer);
}).then(() => {
  return pc2.createAnswer();
}).then(answer => {
  return pc2.setLocalDescription(answer).then(() => answer);
}).then(answer => {
  return pc1.setRemoteDescription(answer);
});

webrtc建立的过程

WebRTC 建立连接的过程可以分为几个关键阶段,从本地媒体获取、信令交换、ICE 候选收集到最终媒体通道建立,整个流程如下所示:

🧭 WebRTC 建立连接的完整流程图解(浏览器到浏览器,P2P):

1. 📷 获取本地媒体

js
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  • 获取用户摄像头和麦克风权限。
  • 获取到的流是一个 MediaStream,可以传给视频标签本地预览和 RTCPeerConnection。

2. 📡 创建 RTCPeerConnection

js
const pc = new RTCPeerConnection(configuration)
  • 可选传入 ICE 配置,如 iceServers(STUN/TURN 服务器)。
  • 用于音视频轨道传输、ICE 协商等。

3. ➕ 添加本地轨道(音视频)

js
stream.getTracks().forEach(track => pc.addTrack(track, stream))
  • 把本地媒体轨道添加到 RTCPeerConnection。
  • 自动触发 offer 的生成。

4. 📤 创建 Offer(由主叫方生成 SDP)

js
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
// 发送 offer.sdp 给远端(通过信令)

5. 📥 接收 Offer 并设置(被叫方)

js
await pc.setRemoteDescription(offer)
// 创建 Answer
const answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
// 发送 answer.sdp 回主叫方
  • 被叫方设置远端描述后,了解了主叫支持哪些 codec。
  • 创建 answer 并设置为本地。

6. 📩 主叫方设置 Answer

js
await pc.setRemoteDescription(answer)
  • 完整建立双向 SDP 会话。
  • 可双向传输媒体信息。

7. ❄️ 交换 ICE 候选(穿透 NAT 的关键)

js
pc.onicecandidate = e => {
  if (e.candidate) {
    sendToPeer('ice-candidate', e.candidate)
  }
}

// 对方收到 ICE 候选后添加
pc.addIceCandidate(candidate)
  • 每端都会收集本地网络候选地址(公网、内网、relay)。
  • 通过信令服务器传递给对方。
  • 最终找到可用的路径建立连接(P2P)。

8. ✅ 媒体通道建立(连接成功)

js
pc.ontrack = e => {
  remoteVideo.srcObject = e.streams[0]
}

🔗 整体流程图概览:

    A(主叫)                          B(被叫)
 ────────────────     信令通道      ────────────────
 getUserMedia()                        getUserMedia()
 addTrack()                            addTrack()
 createOffer()  ───────────────>       setRemoteDescription()
 setLocalDescription()                createAnswer()
 gather ICE                           setLocalDescription()
        ▼                                      ▼
 send offer (SDP)  ───────────────>   send answer (SDP)
 receive answer (SDP)                receive offer (SDP)
 setRemoteDescription()              setRemoteDescription()
        ▼                                      ▼
 gather ICE candidates         ⇄    exchange ICE candidates
        ▼                                      ▼
          📶 成功建立 P2P 连接(ontrack 收到远程音视频)

WebRTC 一对一视频通话 Demo

  • 前端:捕获摄像头、建立 RTCPeerConnection、通过 WebSocket 进行信令
  • 后端:Node.js WebSocket 信令服务器
js
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 3000 });
const clients = new Set();

wss.on('connection', ws => {
  clients.add(ws);

  ws.on('message', msg => {
    // 广播给其他客户端
    for (let client of clients) {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(msg);
      }
    }
  });

  ws.on('close', () => {
    clients.delete(ws);
  });
});

console.log('WebSocket signaling server running on ws://localhost:3000');
html
<!DOCTYPE html>
<html>
<head>
  <title>WebRTC Video Chat</title>
  <style>
    video { width: 45%; margin: 10px; background: black; }
  </style>
</head>
<body>
  <h2>WebRTC P2P Video Chat</h2>
  <video id="localVideo" autoplay muted></video>
  <video id="remoteVideo" autoplay></video>

  <script>
    const localVideo = document.getElementById('localVideo');
    const remoteVideo = document.getElementById('remoteVideo');
    const ws = new WebSocket('ws://localhost:3000');

    let localStream;
    const pc = new RTCPeerConnection();

    ws.onmessage = async (event) => {
      const data = JSON.parse(event.data);

      if (data.offer) {
        await pc.setRemoteDescription(new RTCSessionDescription(data.offer));
        const answer = await pc.createAnswer();
        await pc.setLocalDescription(answer);
        ws.send(JSON.stringify({ answer }));
      }

      if (data.answer) {
        await pc.setRemoteDescription(new RTCSessionDescription(data.answer));
      }

      if (data.candidate) {
        try {
          await pc.addIceCandidate(data.candidate);
        } catch (e) {
          console.error('Error adding received ice candidate', e);
        }
      }
    };

    navigator.mediaDevices.getUserMedia({ video: true, audio: true })
      .then(stream => {
        localVideo.srcObject = stream;
        localStream = stream;

        stream.getTracks().forEach(track => pc.addTrack(track, stream));
      });

    pc.onicecandidate = event => {
      if (event.candidate) {
        ws.send(JSON.stringify({ candidate: event.candidate }));
      }
    };

    pc.ontrack = event => {
      remoteVideo.srcObject = event.streams[0];
    };

    ws.onopen = async () => {
      // 1秒延迟是为了等 localStream 设置完成
      setTimeout(async () => {
        const offer = await pc.createOffer();
        await pc.setLocalDescription(offer);
        ws.send(JSON.stringify({ offer }));
      }, 1000);
    };
  </script>
</body>
</html>