先说一个经常被误解的事实:很多人把“WebRTC实现流数据传输”理解成“把RTMP里的推流地址换成WebRTC的推流地址”,这完全是两码事。WebRTC里不存在什么服务器推流URL,它本质上是两个WebRTC端点之间协商出一条加密的实时媒体通道。你一旦理解了这句换,后面所有的推流拉流、信令设计、弱网优化、跟FreeSWITCH之类的媒体网关打通,全都顺了。
这篇文章我打算按我实际做项目的顺序来写:先讲清楚WebRTC传输的底层逻辑,再给一个能直接跑通的浏览器到浏览器Demo,然后说推流和拉流的架构选型,接着聊FreeSWITCH接入时真正要花心思的地方,最后重点把弱网卡顿的优化思路和手段掰开揉碎讲一遍。适合刚接触WebRTC的开发者,也适合已经做了几个Demo但对架构和优化没有系统认知的人。
1. 先看本质:WebRTC流传输不是“传流”,而是“协商出两条UDP通道”
1.1 四层东西各管各的
WebRTC要打通一条流,至少涉及四层内容,如果你把SIP、RTMP那套思维硬套上来,大概率会在信令和媒体协商阶段卡住。
第一层是信令。WebRTC规范有意不定义信令协议,你可以用WebSocket、可以走SIP、可以塞进MQTT,甚至拿UDP自己封装一个都行。信令层只负责交换两类东西:SDP和ICE Candidate。SDP描述的是“我这边能收什么、能发什么、用什么编码、加密指纹是什么”;Candidate描述的是“我可能有哪几条网络路径能到达”。
第二层是连接。双方把Candidate交换之后,各自通过ICE协议做连通性检测,挑出一条能用的路径。如果两台设备都在严格的NAT后面,打洞失败,就还需要一台TURN服务器做中继转发。注意,TURN只是媒体数据的中转,不是业务服务器。
第三层是加密。WebRTC强制全链路加密:先用DTLS握手协商密钥,然后用SRTP传输音视频RTP包。这就是为什么你在浏览器里看WebRTC的信令里会有一个fingerprint字段,那是DTLS证书指纹,两端要用它校验对方的身份。
第四层才是媒体传输。RTP包里面装着Opus音频、VP8/VP9/H264视频帧,一个视频帧可能拆成好几个RTP包;丢包怎么重传、码率怎么调整、延迟和清晰度怎么权衡,都是这一层的问题。
我见过很多人调WebRTC时纠结“为什么我的SDP里没有推流地址”,那是因为他脑海里还是“拉流端去服务器取流”的模型。WebRTC是点对点模型,即使是浏览器推到SFU服务器,浏览器也仅仅是把SFU当作“另一个对端”来协商,而不是往某个地址写数据。
1.2 为什么说SDP是关键中的关键
把SDP打开看,你会发现里面包含了很多信息,挑几个决定性的说:
m=video、m=audio:媒体行,说明我这个端要收发什么媒体。a=sendonly/a=sendrecv/a=recvonly:流通方向。推流端通常设成sendonly,拉流端通常设成recvonly。这个字段很多新手不看,导致出现“双方都通了,但没有画面”的情况。a=rtpmap:编码格式,比如VP8/90000、OPUS/48000/2。a=ice-ufrag和a=ice-pwd:ICE连通性检测的凭证。a=fingerprint:sha-256:DTLS证书指纹。a=setup:actpass:DTLS角色分配,谁做客户端谁做服务端。
信令服务要做的就是把这些SDP准确送到对端,少送一次、多送一遍、顺序反了都会出问题。我自己的项目里,信令消息只定义了三种:offer、answer、candidate,其他事情全部交给RTCPeerConnection内部去处理。这个模型简单,也够用。
1.3 RTCPeerConnection内部帮你扛了很多事
浏览器里的RTCPeerConnection把ICE、DTLS、SRTP、NACK、FEC、拥塞控制全部封装起来了,你基本不用自己碰RTP包。但封装不意味着你可以不关心机制。比如你打开chrome://webrtc-internals,会看到大量googRtt、packetsLost、framesDropped之类的指标,这些指标共同决定了一条流体验的好与坏。
所以我的判断是:做WebRTC流传输,核心能力是理解RTCPeerConnection的协商流程,和对传输质量指标的判断能力。底层编码反而不是瓶颈,因为浏览器已经给你做完了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 最小闭环Demo:一个推流端,一个拉流端,一个十行信令服务
与其一个劲看概念,不如先把最小闭环跑起来。整个过程我用的是浏览器原生RTCPeerConnection,外加一个Node.js的WebSocket信令服务。没有任何第三方WebRTC库。
2.1 推流端代码
推流端要做的事情是:拿到摄像头麦克风,添加到RTCPeerConnection,创建Offer,把Offer和Candidate通过信令发给对端。
javascript复制// publisher.js
const socket = new WebSocket('ws://localhost:8080');
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
});
// 明确只发不收。这个细节能避免链路上出现无意义的回传流
pc.addTransceiver('video', { direction: 'sendonly' });
pc.addTransceiver('audio', { direction: 'sendonly' });
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
stream.getTracks().forEach(track => pc.addTrack(track, stream));
pc.onicecandidate = (e) => {
if (e.candidate) {
socket.send(JSON.stringify({ type: 'candidate', candidate: e.candidate }));
}
};
socket.onopen = async () => {
cons
