WebRTC(Web Real-Time Communication)作为浏览器原生支持的实时通信技术,已成为构建音视频会议系统的核心方案。它无需插件即可实现浏览器间的点对点(P2P)音视频流传输,配合信令服务和媒体服务器,可轻松搭建支持多人参与的会议场景。本文将从技术原理到实战步骤,全面讲解如何使用 WebRTC 实现音视频会议。
一、WebRTC 核心技术基础
在开始实现前,需先理解 WebRTC 的三大核心组件和工作流程,这是构建会议系统的技术基石。
// server.js(Node.js)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 }); // 监听8080端口
// 存储所有连接的客户端
const clients = new Set();
wss.on('connection', (ws) => {
console.log('新客户端连接');
clients.add(ws);
// 接收客户端消息并转发给其他所有客户端
ws.on('message', (message) => {
console.log(`收到消息:${message}`);
clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(message); // 转发消息
}
});
});
// 客户端断开连接时移除
ws.on('close', () => {
console.log('客户端断开连接');
clients.delete(ws);
});
});
console.log('信令服务器已启动:ws://localhost:8080');
启动服务器:node server.js,此时服务器可接收客户端连接并转发消息。
<!DOCTYPE html>
<html>
<head>
<title>WebRTC音视频会议</title>
<style>
.video-container { display: flex; gap: 20px; margin: 20px; }
video { width: 400px; height: 300px; border: 2px solid #333; }
button { padding: 10px 20px; font-size: 16px; margin: 0 10px; }
</style>
</head>
<body>
<div class="video-container">
<!-- 本地视频流 -->
<div>
<h3>本地画面</h3>
<video id="localVideo" autoplay muted></video>
</div>
<!-- 远程视频流 -->
<div>
<h3>远程画面</h3>
<video id="remoteVideo" autoplay></video>
</div>
</div>
<button id="startBtn">开始会议</button>
<button id="callBtn" disabled>发起呼叫</button>
<button id="hangupBtn" disabled>结束会议</button>
<script>
// 1. 初始化变量
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
const startBtn = document.getElementById('startBtn');
const callBtn = document.getElementById('callBtn');
const hangupBtn = document.getElementById('hangupBtn');
let localStream; // 本地媒体流
let peerConnection; // RTCPeerConnection实例
const ws = new WebSocket('ws://localhost:8080'); // 连接信令服务器
// 2. 配置ICE服务器(使用Google公共STUN)
const iceConfig = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
};
// 3. 开始会议:获取本地媒体流
startBtn.addEventListener('click', async () => {
try {
// 获取摄像头和麦克风流(video: true表示开启视频,audio: true表示开启音频)
localStream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720 }, // 配置视频分辨率
audio: true
});
localVideo.srcObject = localStream; // 显示本地视频
startBtn.disabled = true;
callBtn.disabled = false;
hangupBtn.disabled = false;
} catch (err) {
console.error('获取媒体流失败:', err);
alert('请允许浏览器访问摄像头和麦克风!');
}
});
// 4. 发起呼叫:创建RTCPeerConnection并发送offer
callBtn.addEventListener('click', async () => {
// 创建RTCPeerConnection实例
peerConnection = new RTCPeerConnection(iceConfig);
// 监听ICE候选地址,发送给远程端
peerConnection.addEventListener('icecandidate', (e) => {
if (e.candidate) {
// 发送ICE候选(信令消息格式:JSON)
ws.send(JSON.stringify({ type: 'ice', candidate: e.candidate }));
}
});
// 监听远程媒体流,显示到remoteVideo
peerConnection.addEventListener('track', (e) => {
remoteVideo.srcObject = e.streams[0];
});
// 将本地媒体流添加到PeerConnection
localStream.getTracks().forEach((track) => {
peerConnection.addTrack(track, localStream);
});
// 创建offer(SDP请求)
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer); // 设置本地SDP
// 发送offer到远程端(通过信令服务器)
ws.send(JSON.stringify({ type: 'offer', sdp: offer.sdp }));
callBtn.disabled = true;
});
// 5. 接收信令消息(处理offer、answer、ice)
ws.addEventListener('message', async (e) => {
const message = JSON.parse(e.data);
switch (message.type) {
case 'offer':
// 收到offer,创建PeerConnection并回复answer
peerConnection = new RTCPeerConnection(iceConfig);
// 监听ICE候选并发送
peerConnection.addEventListener('icecandidate', (e) => {
if (e.candidate) {
ws.send(JSON.stringify({ type: 'ice', candidate: e.candidate }));
}
});
// 监听远程媒体流
peerConnection.addEventListener('track', (e) => {
remoteVideo.srcObject = e.streams[0];
});
// 添加本地媒体流
localStream.getTracks().forEach((track) => {
peerConnection.addTrack(track, localStream);
});
// 设置远程SDP(offer)并创建answer
await peerConnection.setRemoteDescription(new RTCSessionDescription({
type: 'offer',
sdp: message.sdp
}));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
// 发送answer到发起端
ws.send(JSON.stringify({ type: 'answer', sdp: answer.sdp }));
break;
case 'answer':
// 收到answer,设置远程SDP
await peerConnection.setRemoteDescription(new RTCSessionDescription({
type: 'answer',
sdp: message.sdp
}));
break;
case 'ice':
// 收到ICE候选,添加到PeerConnection
await peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate));
break;
default:
console.log('未知信令类型:', message.type);
}
});
// 6. 结束会议:关闭连接并停止媒体流
hangupBtn.addEventListener('click', () => {
// 关闭PeerConnection
if (peerConnection) {
peerConnection.close();
peerConnection = null;
}
// 停止本地媒体流
localStream.getTracks().forEach((track) => track.stop());
localVideo.srcObject = null;
remoteVideo.srcObject = null;
// 重置按钮状态
startBtn.disabled = false;
callBtn.disabled = true;
hangupBtn.disabled = true;
});
</script>
</body>
</html>
三、扩展到多人音视频会议
上述 2 人方案仅适用于点对点场景,若需支持 3 人及以上会议,需解决P2P 连接数量爆炸(N 人需 N*(N-1)/2 个连接)和带宽消耗过大的问题,此时需引入媒体服务器(SFU)。
了解了webrtc的基本原理后,我开发了一款多人实时音视频会议软件,给大家体验: https://meeting.codeemo.cn
点个赞
社区有个打包桌面应用的,不知道2者结合会怎样
这个开个网页就能用,何必搞个桌面端,用户还需要安装,增加使用成本。
做成客户端,或许可以 调用win系统自动化方面的 实现 远程控制。
哦哦,回头研究下
我觉得纯音频能支持更多人,这是我问ai的:
纯音频 + Mesh 模式 (不用SFU):
主播端在电脑浏览器上,大约能稳定承载 20~30 听众。
如果主播带宽特别好(光纤 >50 Mbps 上行),理论上能撑 50 听众左右,但风险大
就是 2-3个主播,主播维护着所有听众们的Peer链接。听众不用获取本地流,只播放主播的流。
嗯,确实是。现在已经支持了只开麦克风。
不过现在打洞成功率很低,所以还需要中转服务器,中转服务器也需要对应的带宽