跳到主要内容

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)

参数推荐值说明
iceServersSTUN + TURNTURN 必配
iceTransportPolicy'all'排障时设 'relay' 强制走 TURN
bundlePolicy'max-bundle'所有媒体复用一个端口
rtcpMuxPolicy'require'RTP/RTCP 同端口
iceCandidatePoolSize0(默认)设为 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. 状态机

四个状态字段需要分开看:

状态字段工程含义
signalingStateSDP 协商进度have-local-offer 多半信令链路断了
iceGatheringState候选收集complete = 候选收齐
iceConnectionStateICE 连通性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 之前 createOfferSDP 没 m-line
反复 addTrack/removeTrack 切设备协商风暴
不监听 negotiationneeded 自己手动触发漏掉补充协商
网络断开时立刻销毁 PC 重建DTLS 重新握手成本高
signalingState 判断业务可用应该用 connectionState

12. 权威资料