WebRTC 实战:构建视频会议与直播
用户A 信令服务器 用户B | | | |-- createOffer --------->| | | |-- forward offer ------->| | | |-- setRemoteDesc | |<-- createAnswer ---------| |<-- forward answer -------| | | | | |-- ICE candidate (A) --->|-- forward candidate ---->| |<-- ICE candidate (B) ----|-------------------------| | | |<========== 媒体流 / 数据通道直接传输 =============>|
---
## 第一步:获取本地媒体流
任何 WebRTC 应用都从获取用户的摄像头和麦克风开始。使用 `navigator.mediaDevices.getUserMedia()`。
```javascript
async function startLocalStream() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user'
},
audio: true
});
// 将本地画面插入 video 元素
document.getElementById('localVideo').srcObject = stream;
return stream;
} catch (error) {
console.error('获取媒体失败:', error);
}
}
关键要点:
- 必须运行在
https或localhost环境下(安全上下文要求)。 - 用户需要授予摄像头和麦克风权限。
- 可以通过
video约束控制分辨率、帧率、前后摄像头等。
第二步:理解信令与交换 SDP
两个浏览器要建立直连,必须先交换会话描述协议(Session Description Protocol, SDP)信息。SDP 包含媒体格式、编解码器、网络信息等元数据,不包含媒体流本身。
WebRTC 本身没有规定信令传输方式,你需要自己实现信令服务器(常用 WebSocket)。交换过程有两种常见模式:
1. Offer / Answer 模式(一对一通话)
一方创建 Offer,另一方创建 Answer。
// 创建方 (Caller)
const peerConnection = new RTCPeerConnection(configuration);
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
// 通过信令服务器将 offer 发送给对方
// 接收方 (Callee)
await peerConnection.setRemoteDescription(offer);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
// 将 answer 送回发起方
一旦双方都设置了 Local 和 Remote Description,连接就开始协商。
2. Perfect Negotiation 模式(适合会议等复杂场景)
建议所有应用都采用完美协商模式,避免信令状态冲突。详见 WebRTC 官方推荐。
第三步:ICE 与 NAT 穿透
建立 P2P 连接面临的最大障碍是网络地址转换(NAT)和防火墙。WebRTC 使用 ICE(Interactive Connectivity Establishment)框架自动寻找最优传输路径。
- STUN 服务器:帮助获取公网 IP 和端口类型,实现简单穿透。
- TURN 服务器:当点对点无法连接时,作为中继转发媒体流(消耗服务器带宽)。
你需要提供 ICE 服务器配置:
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }, // 免费 STUN
{
urls: 'turn:your-turn-server.com:3478',
username: 'user',
credential: 'password'
}
]
};
当 ICE 收集到候选地址时,通过信令通道发送给远端:
peerConnection.onicecandidate = event => {
if (event.candidate) {
signaling.send({ type: 'candidate', candidate: event.candidate });
}
};
对方收到后添加:
await peerConnection.addIceCandidate(candidate);
第四步:构建一对一视频通话
完整的一对一通话代码骨架:
发起端 (index.html 脚本):
const pc = new RTCPeerConnection(iceConfig);
const localStream = await startLocalStream();
localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
// 处理远端流
pc.ontrack = event => {
remoteVideo.srcObject = event.streams[0];
};
// 创建 Offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send({ type: 'offer', sdp: pc.localDescription });
// 监听来自信令的 Answer
signaling.on('answer', async (answer) => {
await pc.setRemoteDescription(new RTCSessionDescription(answer));
});
signaling.on('candidate', async (candidate) => {
await pc.addIceCandidate(candidate);
});
接收端 流程对称,在收到 offer 后创建 answer 并设置。
此时代码已能实现浏览器间的音视频通话。将本地和远端 <video> 元素显示即可。
第五步:使用 DataChannel 实现文字聊天或文件传输
RTCDataChannel 提供低延迟、非可靠或可靠的数据传输,基于 SCTP 协议。非常适合传输文字、控制指令、或小文件。
创建数据通道(任一 Peer 调用均可):
const dataChannel = pc.createDataChannel('chat');
dataChannel.onopen = () => console.log('数据通道已打开');
dataChannel.onmessage = event => {
console.log('收到消息:', event.data);
};
// 发送文本
dataChannel.send('Hello WebRTC!');
另一端监听:
pc.ondatachannel = event => {
const receiveChannel = event.channel;
receiveChannel.onmessage = e => console.log(e.data);
};
第六步:变身为视频会议(多人音视频)
多人会议的核心挑战是拓扑结构选择。常见方案:
1. Mesh 架构(全连接)
每个参会者和其他所有参会者建立 P2P 连接。适合 3~4 人的小房间,无需服务器中转媒体。
A
/ | \
B--C--D
优点:服务器简单,零媒体成本。
缺点:上行带宽和编码能力随人数线性增长,移动端难承受。
2. SFU 架构(选择性转发单元)
每个人只上传一路流到 SFU 服务器,SFU 负责转发给其他人。是目前最主流方案。
A → \ / → B
B → —[ SFU ] —— → C
C → / \ → D
可以配合 Simulcast(多分辨率上传)和 SVC 优化带宽。需集成 Mediasoup、Janus、Pion 等 SFU 库。
3. MCU 架构(多点控制单元)
服务器将多路流解码、合成为单一视频流推给所有客户端。客户端负载极低,但服务器计算成本高。
建议:学习阶段先用 Mesh 了解原理,生产环境迁移到 SFU。
实现简单 Mesh 会议
维护一个 Peer 列表,每当新用户加入时,已有的所有用户和新用户建立连接。伪代码:
// 信令服务器管理房间成员,发送新成员加入事件
signaling.on('new-peer', async (peerId) => {
const pc = createPeerConnection(peerId);
// 将本地流加入该连接
localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
// 建立 Offer...
});
// 新加入的成员先获取房间内已有成员列表,逐一建立连接
signaling.on('existing-peers', (peerList) => {
peerList.forEach(peerId => createOfferFor(peerId));
});
这种方式可快速验证多人通话功能。
第七步:构建直播推流
直播场景下,主播上传一路流,观众只接收(单向),无需对等连接。核心思路:
- 主播使用 WebRTC 推流到媒体服务器(如 SRS、MediaSoup、Millicast)。
- 观众从媒体服务器拉流(可用 WebRTC 拉,也可转成 HLS/FLV 等低延迟协议)。
最简单实验方案:使用 WebRTC + Mediasoup 搭建推拉流服务。也可以使用现成服务如 Daily、Agora、Twilio 等。
客户端推流代码(主播):
// 与建立普通连接相同,只不过远端是服务器而非其他浏览器
const pc = new RTCPeerConnection();
const stream = await startLocalStream();
stream.getTracks().forEach(track => pc.addTrack(track, stream));
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 将 offer SDP 通过 HTTP/WS 发给你的服务器,获取 Answer
const response = await fetch('/webrtc/publish', {
method: 'POST',
body: JSON.stringify({ sdp: pc.localDescription }),
headers: { 'Content-Type': 'application/json' }
});
const answer = await response.json();
await pc.setRemoteDescription(new RTCSessionDescription(answer.sdp));
观众拉流:从服务器获取 Offer SDP 并应答,然后接收媒体轨道显示。实际服务器会处理好 track 转发。
第八步:常见问题与优化技巧
- 权限问题:确保 HTTPS,或者开发时使用
localhost。部分浏览器还要求用户手势后才能调用 getUserMedia。 - 回声消除:WebRTC 内置音频处理,但使用外部扬声器时可能产生回声。可设置
videoElement.volume = 0防止本地播放捕获回声。 - 带宽控制:可通过
RTCRtpSender.setParameters()调整比特率,或使用 Simulcast 发送多路不同质量流。 - 连接状态监控:监听
iceConnectionState变化,及时提示用户重新连接。 - 移动端适配:考虑屏幕旋转、前后摄像头切换,使用
facingMode约束和orientation事件。 - 调试利器:Chrome 内置
chrome://webrtc-internals,可查看详细连接统计、候选、丢包等。
动手实践:从 Demo 到产品
建议按以下路径进阶练习:
- 本地单页测试:不使用信令,手动交换 SDP(两个浏览器标签页)。熟悉 API。
- 引入 WebSocket 信令:搭建简单 Node.js 信令服务器,实现一对一双向视频。
- 添加 DataChannel:实现文字聊天。
- 扩展为 Mesh 小组会议。
- 集成 SFU:例如使用 Mediasoup 搭建 SFU 服务,替换 Mesh。
- 加入身份认证、房间管理、录制等:完善后端。
示例信令服务器(Node.js + WebSocket):
// 精简版示例仅展示思路
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', ws => {
ws.on('message', data => {
const msg = JSON.parse(data);
// 广播给房间内其他成员(需要维护房间状态)
broadcast(msg, ws);
});
});