WebRTC 实时通信
WebRTC(Web Real-Time Communication)是一项开放标准技术,使浏览器和移动应用无需中间插件即可实现实时音视频通信和数据传输。它由 W3C 和 IETF 联合标准化,已成为现代实时通信领域的基石技术。
音视频采集
getUserMedia
getUserMedia 是 WebRTC 中访问用户媒体设备的核心 API,它返回一个 MediaStream 对象,包含请求的音频和视频轨道。
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
}).then(stream => {
// 使用 stream
}).catch(err => {
// 处理错误
});在现代浏览器中,getUserMedia 始终返回 Promise,调用时会触发浏览器权限请求弹窗,用户授权后才能获取媒体流。常见的错误包括 NotAllowedError(权限被拒绝)、NotFoundError(未找到设备)和 NotReadableError(设备被其他应用占用)。
MediaStream 与 MediaStreamTrack
MediaStream 由多个 MediaStreamTrack 组成,每个 Track 代表单一类型的媒体数据。音频和视频 Track 可以独立控制:
getAudioTracks()—— 获取所有音频轨道getVideoTracks()—— 获取所有视频轨道addTrack()/removeTrack()—— 动态增删轨道clone()—— 克隆流
每个 Track 具有 enabled 和 muted 属性,可分别控制是否发送数据和检测是否被静音。Track 的生命周期可通过 stop() 方法终止。
分辨率和帧率控制
通过 getUserMedia 的 video 约束参数可以精细控制分辨率和帧率:
navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280, min: 640, max: 1920 },
height: { ideal: 720, min: 480, max: 1080 },
frameRate: { ideal: 30, min: 15, max: 60 },
aspectRatio: 16 / 9
},
audio: {
sampleRate: { ideal: 48000 },
channelCount: { ideal: 2 }
}
});约束分为三种类型:exact(精确值)、ideal(理想值,浏览器会尽量满足)和 min/max(范围)。浏览器会根据约束条件选择最合适的配置,但最终结果可能不等于理想值,需要通过 getSettings() 获取实际参数。
设备选择
enumerateDevices API 可以枚举系统中的所有媒体设备:
navigator.mediaDevices.enumerateDevices().then(devices => {
const cameras = devices.filter(d => d.kind === 'videoinput');
const mics = devices.filter(d => d.kind === 'audioinput');
const speakers = devices.filter(d => d.kind === 'audiooutput');
});每个设备包含 deviceId、label、kind 和 groupId 属性。通过指定 deviceId: { exact: selectedDeviceId } 可以切换到特定设备。groupId 相同的设备属于同一物理硬件,便于关联摄像头和麦克风。
屏幕共享
屏幕共享使用 getDisplayMedia API,它与 getUserMedia 不同,专门用于捕获屏幕内容:
async function startScreenShare() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { cursor: 'always' },
audio: false
});
return stream;
} catch (err) {
console.error('屏幕共享失败:', err);
}
}getDisplayMedia 会弹出屏幕选择对话框,用户可选择共享整个屏幕、应用窗口或浏览器标签页。屏幕共享 Track 有一个 ended 事件,当用户停止共享时会触发。在实践中,屏幕共享通常作为独立 Track 添加到 RTCPeerConnection 中,与摄像头 Track 同时传输。
信令机制
信令服务器的作用
WebRTC 本身不提供信令传输机制,需要开发者自行实现信令服务器来协调通信双方的连接建立。信令服务器负责:
- 会话控制 —— 创建/加入/离开房间
- SDP 交换 —— 传递 Offer 和 Answer
- ICE Candidate 转发 —— 传递网络候选者信息
- 用户状态同步 —— 通知参与者加入/离开
WebSocket 实现信令
最常用的信令传输方式是 WebSocket,它提供全双工通信通道:
const ws = new WebSocket('wss://signaling.example.com');
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case 'offer': handleOffer(msg.sdp); break;
case 'answer': handleAnswer(msg.sdp); break;
case 'candidate': handleCandidate(msg.candidate); break;
case 'join': handleUserJoined(msg.userId); break;
case 'leave': handleUserLeft(msg.userId); break;
}
};除了 WebSocket,也可以使用 SSE(Server-Sent Events)进行单向推送,或用长轮询(Long Polling)作为降级方案。
SDP 交换流程
SDP(Session Description Protocol)描述了媒体会话的配置信息,包括编解码器、IP 地址、端口号等。标准交换流程如下:
- 发起方创建
RTCPeerConnection,调用createOffer()生成 SDP Offer - 调用
setLocalDescription(offer)设置本地描述 - 通过信令服务器将 Offer 发送给接收方
- 接收方调用
setRemoteDescription(offer)设置远程描述 - 接收方调用
createAnswer()生成 SDP Answer - 接收方调用
setLocalDescription(answer)设置本地描述 - 通过信令服务器将 Answer 发送回发起方
- 发起方调用
setRemoteDescription(answer)完成交换
简化实现:
// 发起方
const pc = new RTCPeerConnection(config);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
ws.send(JSON.stringify({ type: 'offer', sdp: offer }));
// 接收方
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
ws.send(JSON.stringify({ type: 'answer', sdp: answer }));ICE Candidate 交换
在 SDP 交换之后(或同时),双方还需要交换 ICE Candidate。每当 RTCPeerConnection 发现新的候选者时,icecandidate 事件触发:
pc.onicecandidate = (event) => {
if (event.candidate) {
ws.send(JSON.stringify({
type: 'candidate',
candidate: event.candidate
}));
}
};ICE Candidate 的交换可以与 SDP 交换并行进行,但需要注意时序问题——接收方应在收到 Offer 后就开始处理 Candidate 信息。
房间管理
房间是信令服务器的核心抽象,负责将参与者分组。典型的房间管理逻辑包含:
- 创建房间 —— 生成唯一房间 ID(UUID 或短码),设置创建者为房间主人
- 加入房间 —— 通过房间 ID 加入,房间人数限制(1v1 最多 2 人,会议模式可多人)
- 离开房间 —— 清理房间状态,通知其他参与者
- 房间列表 —— 可选功能,列出可公开加入的房间
服务端房间管理示例逻辑:
const rooms = new Map();
function createRoom(creatorId) {
const roomId = generateShortId();
rooms.set(roomId, {
id: roomId,
creator: creatorId,
participants: [creatorId],
createdAt: Date.now()
});
return roomId;
}
function joinRoom(roomId, userId) {
const room = rooms.get(roomId);
if (!room) throw new Error('房间不存在');
if (room.participants.length >= 2) throw new Error('房间已满');
room.participants.push(userId);
notifyParticipantJoined(room, userId);
}Signaling 协议设计
一个好的信令协议设计需要关注以下几点:
- 消息类型 —— 明确定义 Offer、Answer、Candidate、Join、Leave、Ping/Pong 等消息类型
- 消息顺序 —— 确保 SDP 和 Candidate 按正确顺序处理
- 重连机制 —— 支持 WebSocket 断线重连,恢复会话状态
- 房间保活 —— 心跳检测,超时后自动清理僵尸房间
- 错误处理 —— 明确的错误码和错误消息返回
实践中常用 JSON 格式作为消息载体,每个消息包含 type、roomId、userId 和 payload 字段。对于大规模部署,建议使用消息队列(如 Redis Pub/Sub)来水平扩展信令服务器。
P2P 连接
ICE 框架
ICE(Interactive Connectivity Establishment)是 WebRTC 实现 NAT 穿透的核心框架。它综合使用 STUN 和 TURN 协议,在不同网络环境下找到最优的通信路径。ICE 框架的工作流程分为候选者收集、连通性检查和候选者选定三个阶段。
STUN 与 TURN
STUN(Session Traversal Utilities for NAT) 用于帮助端点在 NAT 后面发现自己的公网地址和端口。客户端向 STUN 服务器发送请求,服务器返回客户端的公网 IP 和端口,这个信息被封装为 srflx 类型的候选者。
TURN(Traversal Using Relays around NAT) 用于在对称 NAT 或防火墙限制严格的环境下,通过中继服务器转发媒体数据。TURN 服务器分配一个中继地址,客户端的数据先发送到 TURN 服务器,再由服务器转发给对方。TURN 的 relay 候选者延迟较高,是最后的选择。
配置 ICE 服务器:
const config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:turn.example.com:3478',
username: 'user',
credential: 'pass'
}
]
};NAT 穿透原理
常见的 NAT 类型包括:
- 完全锥形 NAT —— 任何外部主机都能通过映射地址访问内部主机
- 限制锥形 NAT —— 只有内部主机曾通信过的外部主机才能访问
- 端口限制锥形 NAT —— 在上述基础上限制端口
- 对称 NAT —— 每个外部目标使用不同的映射地址和端口,最难穿透
ICE 协议通过收集多种类型的候选者,逐一进行连通性检查,以找到可用的通信路径。对于对称 NAT,通常需要 TURN 中继才能建立连接。
候选者收集
RTCPeerConnection 在 ICE 过程中会收集三种类型的候选者:
- host 候选者 —— 本地网卡的 IP 地址和端口,优先级最高
- srflx 候选者 —— 通过 STUN 服务器发现的公网映射地址
- relay 候选者 —— TURN 服务器分配的中继地址,优先级最低
候选者收集过程可以通过 icegatheringstatechange 事件跟踪:
pc.onicegatheringstatechange = () => {
console.log('ICE Gathering 状态:', pc.iceGatheringState);
// 取值: 'new' → 'gathering' → 'complete'
};连接状态监控
ICE 连接状态通过 connectionstatechange 事件监控,这是 WebRTC 连接健康度的核心指标:
pc.onconnectionstatechange = () => {
switch (pc.connectionState) {
case 'new':
console.log('新建连接');
break;
case 'connecting':
console.log('正在连接...');
break;
case 'connected':
console.log('已连接');
break;
case 'disconnected':
console.log('连接断开(可尝试恢复)');
break;
case 'failed':
console.log('连接失败');
break;
case 'closed':
console.log('连接已关闭');
break;
}
};更细粒度的 ICE 传输层状态可以通过 iceconnectionstatechange 事件获取。当检测到 disconnected 状态时,可以启动 ICE Restart 尝试恢复连接。
ICE Restart
当网络环境发生变化(如用户切换 WiFi 到移动网络)或连接超时时,需要触发 ICE Restart 来重建连接。ICE Restart 通过创建一个新的 Offer 并设置 iceRestart 标志来实现:
function restartIce() {
const offer = await pc.createOffer({ iceRestart: true });
await pc.setLocalDescription(offer);
// 通过信令发送新的 Offer
ws.send(JSON.stringify({ type: 'offer', sdp: offer }));
}ICE Restart 不会破坏已有的媒体传输通道,双方会重新收集候选者并建立新的连接,然后无缝切换。这个过程对用户应该是透明的。
媒体数据传输
RTP 与 SRTP
RTP(Real-time Transport Protocol) 是 WebRTC 传输音视频数据的底层协议。RTP 报文头部包含序列号、时间戳和 SSRC 标识符,用于数据包排序、抖动缓冲和媒体同步。
SRTP(Secure RTP) 是 RTP 的加密扩展,WebRTC 强制要求所有媒体流使用 SRTP 传输。SRTP 使用 AEAD 加密算法(如 AES-GCM),在传输前对 RTP 净荷进行加密和完整性校验,防止窃听和篡改。
RTP 报文结构:
┌─────────────────────────────┐
│ V=2 │P│X│ CC │M│ PT │
├─────────────────────────────┤
│ 序列号 (16 bits) │
├─────────────────────────────┤
│ 时间戳 (32 bits) │
├─────────────────────────────┤
│ SSRC (32 bits) │
├─────────────────────────────┤
│ RTP 净荷 (编解码器数据) │
└─────────────────────────────┘SCTP 与 DataChannel
SCTP(Stream Control Transmission Protocol) 用于 WebRTC 的 DataChannel,提供可靠或部分可靠的数据传输服务。DataChannel 建立在 SCTP 之上,支持两种模式:
- 可靠模式 —— 保证数据有序到达,类似 TCP,适合文件传输
- 部分可靠模式 —— 允许丢包或乱序,适合实时游戏、位置更新等
DataChannel 使用非常简单:
const channel = pc.createDataChannel('chat', {
ordered: true,
maxRetransmits: 3
});
channel.onmessage = (event) => {
console.log('收到消息:', event.data);
};
channel.send('Hello WebRTC!');每个 RTCPeerConnection 可以创建多个 DataChannel,每个 Channel 由独立的 SCTP 流承载。DataChannel 的 SCTP 连接建立在 DTLS 加密通道之上,确保数据传输的安全性。
编解码器支持
WebRTC 要求的必选编解码器:
| 媒体类型 | 必选编解码器 | 可选编解码器 |
|---|---|---|
| 音频 | Opus, PCMU/PCMA | G.722, iLBC, ISAC |
| 视频 | VP8, H.264 | VP9, H.265, AV1 |
Opus 是 WebRTC 的首选音频编解码器,支持 6 kbps 到 510 kbps 的动态码率范围,采样率从 8 kHz 到 48 kHz,具有极低的算法延迟(5 ms 起)。
VP8 是 WebRTC 的基础视频编解码器,所有浏览器都必须支持。H.264 在硬件编码支持方面有优势,适合移动端。VP9 和 AV1 提供更好的压缩效率,但编码复杂度更高。
编解码器可以通过 setCodecPreferences 调整优先级:
const capabilities = RTCRtpSender.getCapabilities('video');
const selectedCodec = capabilities.codecs.find(c => c.mimeType === 'video/VP9');
if (selectedCodec) {
const sendParams = pc.getTransceivers()[0].sender.getParameters();
sendParams.codecs = [selectedCodec, ...sendParams.codecs];
pc.getTransceivers()[0].sender.setParameters(sendParams);
}Simulcast 与 SVC
Simulcast(同时广播)将同一视频源编码为多个不同分辨率和码率的流独立发送。SFU 服务器根据接收方的带宽和能力选择转发合适的流。例如,一个 Simulcast 配置可以同时发送 1080p、720p 和 360p 三个版本:
const sendParams = sender.getParameters();
sendParams.encodings = [
{ rid: 'high', active: true, maxBitrate: 2_500_000, scaleResolutionDownBy: 1.0 },
{ rid: 'mid', active: true, maxBitrate: 1_000_000, scaleResolutionDownBy: 2.0 },
{ rid: 'low', active: true, maxBitrate: 300_000, scaleResolutionDownBy: 4.0 }
];
sender.setParameters(sendParams);SVC(Scalable Video Coding) 将视频编码为分层结构,包含一个基础层和一个或多个增强层。接收端可以根据带宽条件丢弃增强层,只解码基础层也能获得可观看的视频。VP9 的 SVC 模式称为 Spatial SVC,被广泛用于 WebRTC 的多方通话场景。
Simulcast 的兼容性更好(浏览器原生支持),但带宽开销较大。SVC 在带宽适配方面更灵活,但编码器复杂度更高。
带宽估计
WebRTC 内置了带宽估计算法(GCC——Google Congestion Control),基于两种机制:
- 基于延迟的估计 —— 分析 RTP 报文的到达时间变化,检测网络拥塞
- 基于丢包的估计 —— 根据 RTCP Receiver Report 中的丢包率调整发送码率
带宽估计的结果通过 RTCPeerConnection 的 onBWE 回调(非标准 API)或 RTCRtpSender 的 setParameters 反馈到编码器。开发者也可以通过 RTCRtpSender.getStats() 获取当前带宽估计值:
const stats = await pc.getStats();
stats.forEach(stat => {
if (stat.type === 'candidate-pair' && stat.selected) {
console.log('当前可用带宽:', stat.availableOutgoingBitrate);
}
});SFU/MCU 架构
三种架构对比
在多方实时通信中,根据媒体流的转发和处理方式,主要有三种架构:
| 特性 | Mesh (P2P) | SFU | MCU |
|---|---|---|---|
| 连接数 | O(n²) | O(n) | O(n) |
| 上行带宽 | O(n) | O(1) | O(1) |
| 下行带宽 | O(n) | O(n) | O(1) |
| 客户端负载 | 高 | 低 | 低 |
| 服务端负载 | 无 | 中(转发) | 高(混流) |
| 延迟 | 低 | 低 | 中 |
| 灵活性 | 低 | 高 | 中 |
| 适用人数 | 2~4 人 | 4~50+ 人 | 8~20 人 |
Mesh 架构(P2P)下,每个客户端向其他所有客户端发送上行流,并接收所有其他客户端的下行流。3 人通话需要 6 个连接,4 人需要 12 个。上行带宽随人数线性增长——4 人通话时,每个客户端需要上传 3 路流。Mesh 架构仅适合小规模会议,优点是无需服务器转发成本。
SFU 架构(Selective Forwarding Unit)是目前最主流的多方通话架构。每个客户端只上传一路流(或一组 Simulcast 流),SFU 服务器负责选择性转发。客户端只接收需要的流,下行带宽为 O(n)。SFU 的转发逻辑可以基于接收方带宽、订阅策略或 Simulcast 层级选择。
MCU 架构(Multipoint Control Unit)对服务端性能要求最高,它解码所有输入流,混音混屏后重新编码为一路或几路流发送给客户端。MCU 的优点是客户端负载极低(只需解码一路流),适合老设备或弱网环境。缺点在于混流引入了额外的编解码延迟,且服务端成本高昂。
Selective Forwarding Unit(SFU)
SFU 的核心职责是媒体流的路由和转发。一个典型 SFU 的实现需要处理:
- 流管理 —— 接收发布者的媒体流,创建订阅者的下行流
- Simulcast 层级选择 —— 根据订阅者的带宽选择合适的层级转发
- 带宽适配 —— 动态切换转发层级,避免接收端缓冲
- 关键帧请求 —— 当新加入或切换层级时,请求关键帧
- 音频混音 —— 可选的音频混音功能,减少下行音频流数
常见的 SFU 开源实现包括 mediasoup、Janus、LiveKit 和 SRS。它们各有侧重:mediasoup 灵活轻量,适合定制开发;LiveKit 开箱即用,功能完善;Janus 插件丰富,适合作为媒体网关。
MCU 混流
MCU 的混流分为两个层面:
音频混流:将多路音频解码后叠加,重新编码为单路音频流。需要注意音量归一化、VAD(语音活动检测)和舒适噪声生成。
视频混屏:将多路视频画面拼合成一个合成画面,常见的布局包括平铺布局、演讲者模式(一大几小)和画中画模式。
平铺布局(4人):
┌──────────┬──────────┐
│ 用户 A │ 用户 B │
├──────────┼──────────┤
│ 用户 C │ 用户 D │
└──────────┴──────────┘
演讲者模式:
┌──────────────────────┐
│ │
│ 演讲者(大画面) │
│ │
├──────┬──────┬───────┤
│ B │ C │ D │
└──────┴──────┴───────┘架构选型建议
| 使用场景 | 推荐架构 | 理由 |
|---|---|---|
| 1v1 通话 | 直连 P2P | 无需服务器转发,延迟最低 |
| 4 人以下小会 | Mesh 或 SFU | Mesh 实现简单,SFU 更省带宽 |
| 4~20 人中型会议 | SFU | 带宽可控,客户端压力小 |
| 20~50 人大会 | SFU + Simulcast | 分层编码,按需分发 |
| 50 人以上直播 | SFU + 小窗模式 | 限制活跃发言人数 |
| 弱网环境 | MCU 或 SFU + SVC | 服务端处理,客户端低负载 |
| 录制与转播 | MCU | 单路流易录制 |
WebRTC 安全
DTLS-SRTP 加密
WebRTC 强制要求所有通信使用加密传输。DTLS(Datagram Transport Layer Security)用于在 UDP 上建立加密通道和进行密钥协商:
- DTLS 握手 —— 在 ICE 连接建立后,双方通过 DTLS 握手协商加密密钥
- SRTP 密钥导出 —— DTLS 握手完成后,导出 SRTP 密钥用于媒体加密
- 证书验证 —— 双方交换自签名证书(fingerprint),与 SDP 中的 fingerprint 字段比对
SDP 中的 fingerprint 字段用于证书绑定:
a=fingerprint:sha-256 AA:BB:CC:...:FF
a=setup:actpasssetup 属性决定 DTLS 角色:actpass(主动/被动)、active(主动发起)或 passive(被动接受)。接收方在收到 SDP 后,会验证 DTLS 证书的 fingerprint 是否与 SDP 中声明的值匹配,防止中间人攻击。
加密层次
WebRTC 的加密体系分为三个层次:
- 传输加密 —— DTLS 加密 DataChannel(SCTP over DTLS)和媒体密钥协商
- 媒体加密 —— SRTP 加密音视频数据包,使用 DTLS 导出的密钥
- 身份认证 —— 可选的 WebRTC Identity Provider(IdP)机制,通过 OAuth 令牌验证通信双方身份
所有 WebRTC 连接都默认启用 DTLS 和 SRTP 加密,开发者无需额外配置。与传统的未加密 RTP 不同,WebRTC 的 SRTP 不仅加密净荷,还对 RTP 头部进行部分加密(头部扩展)和完整性保护。
权限管理
浏览器的媒体权限管理遵循以下原则:
- getUserMedia —— 必须通过 HTTPS 或 localhost 调用,需要用户明确的点击授权
- getDisplayMedia —— 每个屏幕共享会话都需要用户选择共享内容并授权
- 持久授权 —— 某些浏览器支持记住授权决定,但摄像头和麦克风权限通常每次会话都需确认
- 权限撤销 —— 用户可随时通过浏览器设置撤销已授权的权限
HTTP 页面只能在 localhost 或 file:// 协议下使用 getUserMedia。生产环境必须使用 HTTPS,否则 navigator.mediaDevices 为 undefined。
来源限制
WebRTC 的安全模型还包含以下来源限制:
- HTTPS 要求 ——
getUserMedia、getDisplayMedia和RTCPeerConnection的某些功能仅在安全上下文中可用 - 跨域策略 —— 页面中的 iframe 需要显式声明
allow属性才能使用媒体设备:html<iframe src="..." allow="camera; microphone; display-capture"></iframe> - 偏好嗅探防护 —— 不允许通过 enumerateDevices 获取设备标签,除非用户已授权媒体权限
- 自签名证书限制 —— 某些浏览器可能限制自签名证书下的 WebRTC 功能
总结
WebRTC 作为现代实时通信的基石技术,提供了从媒体采集、信令交换、P2P 连接到媒体传输的完整解决方案。它内置的加密机制(DTLS-SRTP)确保了通信安全,灵活的架构(Mesh/SFU/MCU)使其能够适应从 1v1 通话到大型会议的不同场景。随着 WebRTC 标准的持续演进(如 AV1 编解码器支持、更高效的带宽估计),其应用边界正在不断扩展。