RTCPeerConnection
RTCPeerConnection 是 WebRTC 的核心控制器:建链、协商、收发媒体、调度带宽都从这里出发。掌握它的状态机和 Transceiver 模型,整个 WebRTC 就拿下一半。
1. 最小可运行模板
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:turn.example.com:3478', username, credential },
],
bundlePolicy: 'max-bundle',
rtcpMuxPolicy: 'require',
iceTransportPolicy: 'all',
});
stream.getTracks().forEach(t => pc.addTrack(t, stream));
pc.addEventListener('track', ({track, streams}) => {/* 收流 */});
pc.addEventListener('icecandidate', ({candidate}) => {/* 发送候选 */});
pc.addEventListener('negotiationneeded', () => {/* 触发 createOffer */});
pc.addEventListener('connectionstatechange', () => {
if (pc.connectionState === 'failed') pc.restartIce();
});
2. 构造参数(RTCConfiguration)
| 参数 | 推荐值 | 说明 |
|---|---|---|
iceServers | STUN + TURN | TURN 必配 |
iceTransportPolicy | 'all' | 排障时设 'relay' 强制走 TURN |
bundlePolicy | 'max-bundle' | 所有媒体复用一个端口 |
rtcpMuxPolicy | 'require' | RTP/RTCP 同端口 |
iceCandidatePoolSize | 0(默认) | 设为 4 可加速首次建连,但占资源 |
certificates | 长期证书 | 默认每次重建 PC 都生成新证书 |
2.1 复用证书减少握手时间
const cert = await RTCPeerConnection.generateCertificate({
name: 'ECDSA', namedCurve: 'P-256',
});
const pc = new RTCPeerConnection({ certificates: [cert], iceServers });
证书可缓存到 IndexedDB,避免每次重建 PC 都生成(生成约 100ms)。
3. Transceiver 模型
WebRTC 1.0 的协商单位不是 Track,而是 Transceiver(一对 sender + receiver)。
RTCRtpTransceiver
├── sender (RTCRtpSender) ── 本端发出去的方向
├── receiver (RTCRtpReceiver) ── 对端发过来的方向
├── direction: 'sendrecv' | 'sendonly' | 'recvonly' | 'inactive'
├── mid: '0' | '1' | ... ── 在 SDP 里的 m-line ID
└── stopped: boolean
3.1 三种创建路径
// 1) addTrack:自动建一个 sendrecv transceiver
pc.addTrack(track, stream);
// 2) addTransceiver:精细控制,推荐用于服务端订阅
const tr = pc.addTransceiver('video', {
direction: 'recvonly',
streams: [],
sendEncodings: [ // Simulcast 三层
{ rid: 'q', maxBitrate: 250_000, scaleResolutionDownBy: 4 },
{ rid: 'h', maxBitrate: 750_000, scaleResolutionDownBy: 2 },
{ rid: 'f', maxBitrate: 2_500_000 },
],
});
// 3) ontrack:对端推流时自动建一个 recvonly transceiver
pc.ontrack = ({transceiver, track}) => {
console.log('收到', transceiver.mid, track.kind);
};
3.2 切方向
// 只发不收(如纯主播)
tr.direction = 'sendonly';
// 暂停(仍占 m-line,恢复快)
tr.direction = 'inactive';
// 关闭(永久)
tr.stop(); // 触发 negotiationneeded
4. 状态机
四个状态字段需要分开看:
| 状态 | 字段 | 工程含义 |
|---|---|---|
| signalingState | SDP 协商进度 | 卡 have-local-offer 多半信令链路断了 |
| iceGatheringState | 候选收集 | complete = 候选收齐 |
| iceConnectionState | ICE 连通性 | failed 触发 restartIce |
| connectionState | 整体(含 DTLS) | 业务侧只看这个 |
4.1 connectionState 状态转移
new → connecting → connected → disconnected → failed
└→ closed (主动关闭)
disconnected 是临时网络抖动,不一定要救(一般 5 秒内自愈)。failed 才需要重建或 ICE Restart。
5. Perfect Negotiation 模板
避免双方同时 offer 时卡死,约定一方 polite:
let makingOffer = false;
let ignoreOffer = false;
const polite = false; // 双方约定一个角色
pc.onnegotiationneeded = async () => {
try {
makingOffer = true;
await pc.setLocalDescription();
sendSignal({ description: pc.localDescription });
} finally {
makingOffer = false;
}
};
async function onSignal({description, candidate}) {
if (description) {
const offerCollision =
description.type === 'offer' &&
(makingOffer || pc.signalingState !== 'stable');
ignoreOffer = !polite && offerCollision;
if (ignoreOffer) return;
await pc.setRemoteDescription(description);
if (description.type === 'offer') {
await pc.setLocalDescription();
sendSignal({ description: pc.localDescription });
}
} else if (candidate) {
try { await pc.addIceCandidate(candidate); }
catch (e) { if (!ignoreOffer) throw e; }
}
}
完整推导见 03-信令与NAT穿透/Perfect-Negotiation模式.md。
6. 控制带宽与码率
不要碰 SDP b=AS:,用标准 API:
const sender = pc.getSenders().find(s => s.track?.kind === 'video');
const params = sender.getParameters();
params.encodings[0].maxBitrate = 1_500_000;
params.encodings[0].maxFramerate = 30;
params.encodings[0].scaleResolutionDownBy = 1;
await sender.setParameters(params);
setParameters 不会触发重新协商,立即生效。
Simulcast 多层发送
pc.addTransceiver(track, {
sendEncodings: [
{ rid: 'q', active: true, maxBitrate: 200_000, scaleResolutionDownBy: 4 },
{ rid: 'h', active: true, maxBitrate: 600_000, scaleResolutionDownBy: 2 },
{ rid: 'f', active: true, maxBitrate: 2_000_000 },
],
});
详见 04-媒体引擎与QoS/Simulcast与SVC.md。
7. 切换 Track(不重新协商)
const sender = pc.getSenders().find(s => s.track?.kind === 'video');
await sender.replaceTrack(newTrack);
适用:切摄像头、切屏幕共享、虚拟背景开关。
8. 优雅关闭
function closePeer(pc) {
pc.getSenders().forEach(s => {
try { s.track?.stop(); } catch {}
});
pc.getTransceivers().forEach(t => {
try { t.stop(); } catch {}
});
pc.close();
}
不停止 Track,硬件灯仍亮;不 pc.close(),会有 ICE 心跳泄漏。
9. ICE Restart
网络切换(4G ↔ Wi-Fi、IP 变化)时 ICE 检查会失败,需要 restart:
pc.addEventListener('iceconnectionstatechange', async () => {
if (pc.iceConnectionState === 'failed') {
pc.restartIce(); // 自动触发 negotiationneeded
}
});
restartIce() 比重建 PC 快得多,DTLS 不重新握手。
10. 常用排障命令
// 取一份精简 stats
const stats = await pc.getStats();
for (const r of stats.values()) {
if (r.type === 'inbound-rtp' && r.kind === 'video') {
console.log('丢包率', r.packetsLost / r.packetsReceived);
console.log('卡顿', r.framesDecoded, r.framesDropped);
}
}
// 强制走 TURN 测试
const pcRelay = new RTCPeerConnection({ iceServers, iceTransportPolicy: 'relay' });
详见 04-媒体引擎与QoS/getStats全字段速查.md。
11. 反模式
| 反模式 | 后果 |
|---|---|
在 addTrack 之前 createOffer | SDP 没 m-line |
反复 addTrack/removeTrack 切设备 | 协商风暴 |
不监听 negotiationneeded 自己手动触发 | 漏掉补充协商 |
| 网络断开时立刻销毁 PC 重建 | DTLS 重新握手成本高 |
用 signalingState 判断业务可用 | 应该用 connectionState |
12. 权威资料
- W3C WebRTC 1.0 §4 RTCPeerConnection: https://www.w3.org/TR/webrtc/#peer-to-peer-connections
- W3C WebRTC §5 RTP Media: https://www.w3.org/TR/webrtc/#rtp-media-api
- WebRTC samples: https://webrtc.github.io/samples/
- 核对日期:2026-06-22