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>