WebRTC(Web Real-Time Communication)作为浏览器原生支持的实时通信技术,已成为构建音视频会议系统的核心方案。它无需插件即可实现浏览器间的点对点(P2P)音视频流传输,配合信令服务和媒体服务器,可轻松搭建支持多人参与的会议场景。本文将从技术原理到实战步骤,全面讲解如何使用 WebRTC 实现音视频会议。
一、WebRTC 核心技术基础
在开始实现前,需先理解 WebRTC 的三大核心组件和工作流程,这是构建会议系统的技术基石。
核心 API 与组件
WebRTC 提供了三组核心 API,分别负责媒体捕获、P2P 连接和数据传输:
MediaStream(媒体流):通过getUserMedia()或getDisplayMedia()获取设备摄像头、麦克风数据(音视频流),或捕获屏幕内容(用于屏幕共享)。
RTCPeerConnection(对等连接):浏览器间建立 P2P 连接的核心,负责媒体流的编码、传输和解码,支持 NAT 穿透(通过 ICE 协议)和带宽自适应。
RTCDataChannel(数据通道):在 P2P 连接基础上传输非媒体数据(如会议控制指令、文字消息),延迟低且支持可靠传输。
关键协议与概念
SDP(Session Description Protocol):描述媒体会话的元数据(如编码格式、传输协议、网络地址),用于浏览器间协商通信参数(例如 A 端发送offer,B 端回复answer)。
ICE(Interactive Connectivity Establishment):解决 NAT 穿透问题的协议,通过 STUN 服务器获取公网 IP,或通过 TURN 服务器转发流量(当 P2P 连接失败时)。
信令服务(Signaling Server):WebRTC 本身不处理信令,需额外搭建信令服务(如基于 WebSocket),用于传递 SDP 消息、ICE 候选地址,以及管理会议成员(加入 / 离开)。
二、音视频会议实现步骤(基于浏览器 + Node.js)
本节以 “2 人基础会议” 为例,逐步讲解从环境搭建到功能实现的完整流程,后续可扩展至多人场景。
环境准备
前端:支持 WebRTC 的现代浏览器(Chrome、Firefox、Edge),使用navigator.mediaDevicesAPI 获取媒体流,通过RTCPeerConnection建立连接。
后端:Node.js + WebSocket(推荐ws库),搭建信令服务器,负责转发客户端间的信令消息。
依赖工具:STUN 服务器(推荐使用 Google 公共 STUN:stun:stun.l.google.com:19302),测试阶段可无需自建;多人场景需额外部署 TURN 服务器(如coturn)。
步骤 1:搭建信令服务器(Node.js + ws)
信令服务器的核心作用是 “转发消息”,不处理媒体流。以下是简化版实现代码:
// 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,此时服务器可接收客户端连接并转发消息。
步骤 2:前端实现(媒体流获取 + P2P 连接)
前端需完成三个核心功能:获取本地媒体流、发送 / 接收信令、建立 P2P 连接并传输音视频。以下是完整 HTML+JS 代码(index.html):
.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; }
本地画面
远程画面
// 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;
});
测试 2 人会议
启动信令服务器:node server.js;
用两个浏览器窗口打开index.html(如 Chrome 的两个标签页,或 Chrome+Firefox);
两个窗口均点击 “开始会议”,允许浏览器访问摄像头和麦克风;
在其中一个窗口点击 “发起呼叫”,另一个窗口会自动接收并显示远程视频,此时 2 人音视频会议已建立。
三、扩展到多人音视频会议
上述 2 人方案仅适用于点对点场景,若需支持 3 人及以上会议,需解决P2P 连接数量爆炸(N 人需 N*(N-1)/2 个连接)和带宽消耗过大的问题,此时需引入媒体服务器(SFU)。
多人会议核心方案:SFU(Selective Forwarding Unit)
SFU 是一种轻量级媒体服务器,工作原理如下:
每个参会者将自己的媒体流发送到 SFU;
SFU 根据参会者列表,将媒体流转发给其他所有成员;
参会者只需接收 N-1 路流(而非发送 N-1 路),大幅减少带宽和连接数。
常用 SFU 框架推荐
Mediasoup:开源 SFU 库,支持 WebRTC 协议,可集成到 Node.js/Java 项目,支持屏幕共享、混音等功能,文档完善且社区活跃;
Janus:C 语言编写的开源媒体服务器,支持 WebRTC、RTSP 等多种协议,适合复杂场景(如直播 + 会议结合);
Pion WebRTC:Go 语言实现的 WebRTC 库,可快速构建轻量级 SFU,性能优异。
多人会议实现思路(以 Mediasoup 为例)
部署 Mediasoup SFU 服务器:基于 Node.js 启动 Mediasoup 服务,配置 Worker(媒体处理进程)、Router(路由)和 Transport(传输通道);
前端与 SFU 交互:
客户端通过信令服务请求加入会议,获取 SFU 的 Transport 信息;
客户端与 SFU 建立 WebRTC 连接(Producer角色,发送本地流到 SFU);
客户端向 SFU 请求订阅其他参会者的流(Consumer角色,接收 SFU 转发的远程流);
信令服务扩展:增加 “会议房间管理” 功能(创建房间、加入房间、离开房间),同步参会者列表和流状态。
四、进阶优化与注意事项
提升会议质量的优化手段
带宽自适应:通过RTCPeerConnection的addTransceiver()配置direction和bandwidth参数,或使用 SFU 的动态码率调整(如根据网络延迟调整视频分辨率);
回声消除与降噪:WebRTC 内置基础回声消除,但复杂场景需结合第三方库(如WebRTC Audio Processing);
屏幕共享:通过navigator.mediaDevices.getDisplayMedia({ video: true })获取屏幕流,与摄像头流切换逻辑结合;
断线重连:监听RTCPeerConnection的connectionstatechange事件,当状态变为disconnected时,通过信令服务重新建立连接。
兼容性与部署注意事项
浏览器兼容性:WebRTC 在主流浏览器中支持良好,但需注意部分 API 差异(如 Safari 需开启getUserMedia权限),可通过caniuse.com查询最新支持情况;
HTTPS 与localhost例外:浏览器要求 WebRTC 仅在 HTTPS 环境下工作(localhost除外,适合开发测试),生产环境需部署 SSL 证书;
TURN 服务器必要性:当参会者处于严格 NAT 环境(如企业内网)时,P2P 连接可能失败,需部署 TURN 服务器(如coturn)转发流量,确保通信稳定性。
五、总结
WebRTC 为音视频会议提供了浏览器原生的实时通信能力,从 2 人 P2P 场景到多人 SFU 场景,均可基于其核心 API 扩展实现。核心步骤可归纳为:
搭建信令服务,实现客户端间的消息转发;
前端通过getUserMedia获取媒体流,通过RTCPeerConnection建立 P2P 连接;
多人场景引入 SFU 服务器,解决连接数和带宽问题;
结合优化手段(如带宽自适应、TURN 服务器)提升会议稳定性和体验。
通过本文的基础方案和扩展思路,开发者可快速搭建符合需求的音视频会议系统,后续可进一步集成聊天、文件共享、会议录制等功能,构建完整的协作平台。
了解了webrtc的基本原理后,我开发了一款多人实时音视频会议软件,给大家体验: https://meeting.codeemo.cn