从 0 到 1 跑通 WebRTC:一份极度重视实践的入门指南

afcppe 发布于 2025-05-14 7 次阅读



0. 为什么这篇博客值得读?

网上 90% 的 WebRTC 文章都在讲「STUN、TURN、ICE、SDP、NAT 打洞」这些概念,却很少有人告诉你:
“把第一路视频跑起来到底需要几行代码?”
本文反其道而行之:

  • 先给你一段 30 行代码,让你 3 分钟看到本地画面;
  • 再给你一段 60 行代码,让你 5 分钟看到对端画面;
  • 最后才解释背后的原理。
    所有代码均可直接复制运行,不依赖第三方库(除浏览器本身)。

1. 环境准备(30 秒)

工具版本说明
Chrome / Edge≥ 90支持最新 WebRTC API
Node.js≥ 18用于跑一个极简信令服务器
网络任意不要求公网 IP,本地 127.0.0.1 即可
# 1. 克隆示例仓库(含全部代码)
git clone https://github.com/yourname/webrtc-practice.git
cd webrtc-practice

# 2. 安装依赖(只有一个 ws 库)
npm install

# 3. 启动信令服务器
node server.js
# 终端输出:Signaling server listening on ws://localhost:8080

2. 30 行代码:把本地摄像头拉到 <video>

新建 01-local-camera.html,直接双击打开即可:

<!doctype html>
<title>01 本地摄像头</title>
<video id="v" autoplay muted width="320"></video>
<script>
navigator.mediaDevices.getUserMedia({video:true, audio:true})
  .then(stream => v.srcObject = stream)
  .catch(console.error);
</script>

效果:浏览器弹出权限询问 → 允许后看到自己和自己的声音。
要点

  • getUserMedia 返回的是 MediaStream,直接赋给 <video>srcObject 即可播放。
  • muted 防止本地回声。

3. 60 行代码:让两个浏览器互相看到对方

3.1 运行信令服务器(已在上一步启动)

server.js 只有 30 行,用 WebSocket 做房间信令:

// server.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', ws => {
  ws.on('message', msg => {
    // 广播给除自己外的所有人(即房间里的另一个客户端)
    wss.clients.forEach(c => {
      if (c !== ws && c.readyState === WebSocket.OPEN) c.send(msg);
    });
  });
});

3.2 网页端代码

新建 02-peer-to-peer.html

<!doctype html>
<title>02 点对点通话</title>
<button id="start">开始通话</button>
<video id="local" autoplay muted width="160"></video>
<video id="remote" autoplay width="160"></video>
<script>
const ws = new WebSocket('ws://localhost:8080');
const pc = new RTCPeerConnection({
  iceServers: [{urls:'stun:stun.l.google.com:19302'}] // 免费 STUN
});

ws.onmessage = async ({data}) => {
  const msg = JSON.parse(data);
  if (msg.sdp) {
    await pc.setRemoteDescription(msg.sdp);
    if (msg.sdp.type === 'offer') {
      await pc.setLocalDescription(await pc.createAnswer());
      ws.send(JSON.stringify({sdp: pc.localDescription}));
    }
  } else if (msg.ice) {
    pc.addIceCandidate(msg.ice);
  }
};

start.onclick = async () => {
  const stream = await navigator.mediaDevices.getUserMedia({video:true, audio:true});
  local.srcObject = stream;
  stream.getTracks().forEach(t => pc.addTrack(t, stream));

  pc.onicecandidate = e => e.candidate && ws.send(JSON.stringify({ice: e.candidate}));
  pc.ontrack = e => remote.srcObject = e.streams[0];

  // 第一个进入房间的人创建 Offer
  const isCaller = location.hash === '#caller';
  if (isCaller) {
    await pc.setLocalDescription(await pc.createOffer());
    ws.send(JSON.stringify({sdp: pc.localDescription}));
  }
};
</script>

3.3 运行步骤

  1. 打开两个浏览器标签页:
  • 标签 A:http://localhost:8000/02-peer-to-peer.html#caller
  • 标签 B:http://localhost:8000/02-peer-to-peer.html
  1. 在 A 点击「开始通话」,B 会自动出现远程画面。
  2. 恭喜,你已完成第一次 WebRTC 通话!

4. 把代码拆成可维护的工程(实战技巧)

真实项目不会把所有逻辑写在 HTML 里。下面给出一个最小可扩展架构:

src/
 ├─ signaling.js      // 信令层(WebSocket / Socket.IO / Firebase)
 ├─ peer.js           // 对 RTCPeerConnection 的封装
 ├─ device.js         // 摄像头、麦克风枚举与切换
 └─ ui.js             // 纯 UI,不碰 WebRTC

示例:peer.js 核心 20 行

// peer.js
export class Peer extends EventTarget {
  constructor() {
    super();
    this.pc = new RTCPeerConnection({iceServers:[{urls:'stun:stun.l.google.com:19302'}]});
    this.pc.ontrack = e => this.dispatchEvent(new CustomEvent('remote', {detail:e.streams[0]}));
    this.pc.onicecandidate = e => e.candidate && this.dispatchEvent(new CustomEvent('ice', {detail:e.candidate}));
  }
  async createOffer(stream) {
    stream.getTracks().forEach(t => this.pc.addTrack(t, stream));
    await this.pc.setLocalDescription(await this.pc.createOffer());
    return this.pc.localDescription;
  }
  async acceptAnswer(sdp) { await this.pc.setRemoteDescription(sdp); }
  async acceptOffer(sdp, stream) {
    await this.pc.setRemoteDescription(sdp);
    stream.getTracks().forEach(t => this.pc.addTrack(t, stream));
    await this.pc.setLocalDescription(await this.pc.createAnswer());
    return this.pc.localDescription;
  }
  addIce(ice) { this.pc.addIceCandidate(ice); }
}

业务层只需关心事件:

const peer = new Peer();
peer.addEventListener('remote', e => remoteVideo.srcObject = e.detail);

5. 常见问题排查清单(实践总结)

现象90% 的原因1 行命令验证
黑屏没拿到摄像头权限chrome://settings/content/camera 检查
建立连接慢没走直连,走了 TURNchrome://webrtc-internals/candidate-pair
听不到声音远端 <video> 没加 autoplay 或被浏览器拦截控制台看 DOMException: play() failed
跨终端失败信令服务器没做房间隔离server.js 里加 roomId 逻辑

6. 进阶路线图

  1. 屏幕共享:把 getUserMedia 换成 getDisplayMedia,一行代码。
  2. 多对多:每个 Peer 都与其他人建 PeerConnection,或改用 SFU(如 mediasoup)。
  3. 移动端:使用 Capacitor / React Native WebRTC 插件,API 完全一致。
  4. 服务端录制:启动一个 headless Chrome,把 remote 标签页录下来即可。

7. 一句话总结

WebRTC 的门槛从来不是协议,而是“第一帧画面”的心理障碍。
跑通本文示例后,再回头看 STUN/TURN/ICE,你会豁然开朗。

Happy hacking!

此作者没有提供个人介绍。
最后更新于 2025-10-13