WebRTC技术解析
一个无需插件、开箱即用的 P2P 音视频传输方案
什么是 WebRTC
WebRTC(Web Real-Time Communication)是 Google 开源的一项实时通信技术,直接内置于浏览器,无需安装任何插件或第三方软件,就能实现浏览器之间的音视频通话、数据传输和屏幕共享。
核心指标 :端到端延迟可控制在 200ms 以内 ,相比传统基于服务器的轮询方案延迟降低 90% ,且服务器带宽成本可减少 70% 以上(因为媒体流不经过服务器中转)。
解决了什么问题
传统网页实现视频通话需要:
-
浏览器插件(Flash、Silverlight)→ 用户需手动安装,已淘汰
-
RTMP + Flash 流媒体服务器 → 延迟高、维护复杂
-
Socket.io + 服务端转发 → 服务器压力大,无法承载大规模并发
WebRTC 的核心突破:
-
NAT 穿透 :通过 ICE(交互式连接建立)协议自动尝试 STUN/TURN 服务器,成功率可达 95% 以上
-
端到端加密:基于 DTLS 和 SRTP,媒体流传输过程全程加密
-
自适应码率:根据网络状况动态调整视频分辨率、帧率和码率,弱网下仍能保持通话
-
直接 P2P 传输 :媒体流不经过服务器,降低服务端带宽消耗 80% 以上
最简单的使用流程
WebRTC 连接建立需要信令服务器 (可用 Socket.io 实现)来交换连接信息,但实际媒体流是 P2P 的。
1. 获取本地媒体流
javascript
// 获取摄像头和麦克风
const localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// 将流绑定到 video 元素
document.getElementById('localVideo').srcObject = localStream;
2. 创建 RTCPeerConnection 并添加流
javascript
// 配置 STUN 服务器(用于 NAT 穿透)
const configuration = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
};
const peerConnection = new RTCPeerConnection(configuration);
// 将本地流的所有 track 添加到连接中
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
// 监听远程流并播放
peerConnection.ontrack = (event) => {
document.getElementById('remoteVideo').srcObject = event.streams[0];
};
3. 创建 Offer 并交换 SDP
发起方(Caller):
javascript
// 创建 offer(包含本地媒体能力描述)
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
// 通过信令服务器发送 offer 给对端
signalingSocket.emit('offer', { offer });
接收方(Callee):
javascript
// 收到 offer 后设置远程描述,并创建 answer
await peerConnection.setRemoteDescription(offer);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
// 发送 answer 回对端
signalingSocket.emit('answer', { answer });
发起方收到 answer:
javascript
await peerConnection.setRemoteDescription(answer);
// 此时连接建立完成,ontrack 会触发
4. ICE 候选交换
连接过程中,浏览器会自动收集 ICE 候选(即可能的网络路径),双方需互相交换:
javascript
javascript
// 监听本地 ICE 候选
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
signalingSocket.emit('ice-candidate', { candidate: event.candidate });
}
};
// 收到对端候选时添加
signalingSocket.on('ice-candidate', async ({ candidate }) => {
await peerConnection.addIceCandidate(candidate);
});
完整的最小 Demo(配合 Socket.io 信令)
服务端(server.js):
javascript
const io = require('socket.io')(3000);
io.on('connection', (socket) => {
socket.on('offer', (data) => socket.broadcast.emit('offer', data));
socket.on('answer', (data) => socket.broadcast.emit('answer', data));
socket.on('ice-candidate', (data) => socket.broadcast.emit('ice-candidate', data));
});
客户端核心代码(省略 HTML 元素绑定):
javascript
// 统一封装信令处理
const socket = io('http://localhost:3000');
let pc = null;
async function startCall(isInitiator) {
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
document.getElementById('localVideo').srcObject = stream;
const config = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
pc = new RTCPeerConnection(config);
stream.getTracks().forEach(t => pc.addTrack(t, stream));
pc.ontrack = e => document.getElementById('remoteVideo').srcObject = e.streams[0];
pc.onicecandidate = e => e.candidate && socket.emit('ice-candidate', e.candidate);
if (isInitiator) {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.emit('offer', { offer });
}
socket.on('offer', async ({ offer }) => {
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit('answer', { answer });
});
socket.on('answer', async ({ answer }) => {
await pc.setRemoteDescription(answer);
});
socket.on('ice-candidate', async ({ candidate }) => {
await pc.addIceCandidate(candidate);
});
}
// 页面加载后决定是否作为发起方(例如通过 URL 参数)
startCall(window.location.search.includes('initiator=true'));