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 运行步骤
- 打开两个浏览器标签页:
- 标签 A:
http://localhost:8000/02-peer-to-peer.html#caller - 标签 B:
http://localhost:8000/02-peer-to-peer.html
- 在 A 点击「开始通话」,B 会自动出现远程画面。
- 恭喜,你已完成第一次 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 检查 |
| 建立连接慢 | 没走直连,走了 TURN | chrome://webrtc-internals/ 看 candidate-pair |
| 听不到声音 | 远端 <video> 没加 autoplay 或被浏览器拦截 | 控制台看 DOMException: play() failed |
| 跨终端失败 | 信令服务器没做房间隔离 | 在 server.js 里加 roomId 逻辑 |
6. 进阶路线图
- 屏幕共享:把
getUserMedia换成getDisplayMedia,一行代码。 - 多对多:每个 Peer 都与其他人建 PeerConnection,或改用 SFU(如 mediasoup)。
- 移动端:使用 Capacitor / React Native WebRTC 插件,API 完全一致。
- 服务端录制:启动一个 headless Chrome,把
remote标签页录下来即可。
7. 一句话总结
WebRTC 的门槛从来不是协议,而是“第一帧画面”的心理障碍。
跑通本文示例后,再回头看 STUN/TURN/ICE,你会豁然开朗。
Happy hacking!
Comments NOTHING