先说一个经常被误解的事实:很多人把“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 () => {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.send(JSON.stringify({ type: 'offer', sdp: pc.localDescription }));
};
socket.onmessage = async (evt) => {
const msg = JSON.parse(evt.data);
if (msg.type === 'answer') {
await pc.setRemoteDescription(msg.sdp);
}
};
2.2 拉流端代码
拉流端的逻辑刚好反过来:收到Offer后创建Answer,收到Candidate后直接addIceCandidate。关键在于ontrack回调里把拿到的MediaStream挂到<video>标签上。
javascript复制// subscriber.js
const socket = new WebSocket('ws://localhost:8080');
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
});
// 明确只收不发
pc.addTransceiver('video', { direction: 'recvonly' });
pc.addTransceiver('audio', { direction: 'recvonly' });
pc.ontrack = (e) => {
const videoEl = document.getElementById('remoteVideo');
if (videoEl.srcObject !== e.streams[0]) {
videoEl.srcObject = e.streams[0];
}
};
pc.onicecandidate = (e) => {
if (e.candidate) {
socket.send(JSON.stringify({ type: 'candidate', candidate: e.candidate }));
}
};
socket.onopen = async () => {
// 等待推流端offer
};
socket.onmessage = async (evt) => {
const msg = JSON.parse(evt.data);
if (msg.type === 'offer') {
await pc.setRemoteDescription(msg.sdp);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.send(JSON.stringify({ type: 'answer', sdp: pc.localDescription }));
} else if (msg.type === 'candidate') {
await pc.addIceCandidate(msg.candidate);
}
};
2.3 信令服务
信令服务本身不碰媒体数据,只负责转发消息。我在本地用的是ws库,几十行就能搞定。
javascript复制// signaling-server.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
let publisher = null;
let subscriber = null;
wss.on('connection', (ws) => {
// 简单起见:第一个连上的是推流端,第二个是拉流端
if (!publisher) {
publisher = ws;
publisher.send(JSON.stringify({ type: 'info', message: 'you are publisher' }));
} else {
subscriber = ws;
subscriber.send(JSON.stringify({ type: 'info', message: 'you are subscriber' }));
}
ws.on('message', (data) => {
const msg = JSON.parse(data);
// 推流端的offer发给拉流端,拉流端的answer发给推流端
if (ws === publisher && subscriber && msg.type === 'offer') {
subscriber.send(JSON.stringify({ type: 'offer', sdp: msg.sdp }));
} else if (ws === subscriber && publisher && msg.type === 'answer') {
publisher.send(JSON.stringify({ type: 'answer', sdp: msg.sdp }));
} else {
// candidate请求对端:做判断,原样转给另一端即可
const target = ws === publisher ? subscriber : publisher;
if (target) {
target.send(JSON.stringify({ type: 'candidate', candidate: msg.candidate }));
}
}
});
});
2.4 跑起来后看什么
跑通之后不要急着收工。打开chrome://webrtc-internals,你会看到几组关键数据:
ICE Connection State:最终到没到connected,如果卡在checking,多半是Candidate没交换完整或者STUN不通。RTT:发包到对端返回确认的耗时,局域网内应该低于20ms。Frames Decoded:是否在持续增长,如果不长,说明远端没有视频帧进来。Packets Lost:在无拥塞的局域网里,这个数字应该接近0。
我自己的经验是,Demo跑起来只需要十分钟,但很多人跑通后没有确认这些指标,直接部署到公网,结果卡成PPT还不知道去哪儿排查。
3. “推流”和“拉流”背后的架构岔路口:P2P、Mesh、SFU选哪个
3.1 点对点只适合极少数场景
第一版Demo是点对点。这种模式适合一对一视频通话、远程协助、两个设备间的数据交换。它的优点是服务器不背媒体流量,成本极低。但只要你超过两路流,问题马上出现:每个参与者的上行带宽和CPU都要承担所有其他参与者的编码任务,呈指数增长,所以点对点多方通信基本撑不过四五个人。
3.2 SFU才是WebRTC场景下真正的“服务器推流/拉流”
直播、在线课堂、连麦这种一对多或多对多的场景,几乎清一色选SFU(Selective Forwarding Unit)。SFU设备收到推流端的RTP流之后,不混流、不转码,直接按需转发给各路拉流端。这样做有几个很实际的好处:
- 推流端只需要上传一路流,下行带宽由SFU承担。
- 拉流端可以根据自己的网络状况,让SFU转发不同质量的流,这就是Simulcast的用武之地。
- 丢包重传、FEC这些弱网对抗动作可以在SFU上做集中处理。
业界常见的SFU有Janus、mediasoup、Licode,商用方案也有不少。它们和浏览器之间的信令,本质上还是我上面说的offer/answer/candidate那套,只是信令对端从另一个浏览器变成了SFU进程而已。
我自己搭过mediasoup,也调过Janus,如果让我给一个比较稳妥的起步方案,小规模并发用Janus或者mediasoup都行,它们都能处理推流和拉流。难点在于你要把业务逻辑和媒体逻辑分开:媒体进程只做流的接入和转发,房间管理、鉴权、录制这些应该放到业务服务里。
3.3 什么时候才会用到MCU转码
SFU不转码,所以遇到终端类型碎片化的时候就会尴尬。比如老款手机不支持VP9、某些硬件编码器只出H264,如果SFU收到VP8,又必须发给只支持H264的端,那就需要MCU(Multipoint Control Unit)来做转码。MCU的CPU/GPU开销远大于SFU,但是兼容性最好。
我建议用这个优先级去判断:能靠策略层面规避的兼容性,不要上MCU;能用SFU转发解决的,不要自己造轮子;真正需要转码或合流的业务(比如混合录制、旁路直播到CDN),再加MCU或者接FFmpeg做转码。
3.4 Demo如何升级成带服务器的推拉流
如果你想把上面的点对点Demo改成“推到SFU”,流程大概是:
- 推流端请求业务服务器创建一个房间。
- 业务服务器让SFU预分配一个transport,并将SFU产生的offer或需要应答的信令发回推流端。
- 推流端跟SFU完成offer/answer协商。
- 其他拉流端加入房间时,业务服务器让SFU为拉流端再创建一条transport,并将订阅信息下发给SFU。
这个流程里,业务服务器始终不碰媒体包,它做的是“让谁和谁协商”这件事。理解了这一点,你就明白了为什么WebRTC项目里,业务服务的代码量和媒体服务的代码量往往是两个量级。
4. 跟FreeSWITCH这类媒体网关互通:难点不在WebRTC,而在SIP和编码的血统差异
4.1 FreeSWITCH在WebRTC链路里扮演什么角色
FreeSWITCH本身是个软交换/媒体服务器,不是专门为WebRTC设计的SFU。但在很多呼叫中心、融合通信项目里,我们需要让浏览器里的WebRTC客户端跟传统的SIP终端通电话,或者让WebRTC流进入FreeSWITCH的IVR、会议、录音流程,这时候就得跟FreeSWITCH做互通。
互通有两个层面的意思:
- 信令层面:浏览器WebRTC客户端可以通过SIP over WebSocket(即
sip.js这类库)注册到FreeSWITCH的sofia profile上,把WebRTC终端的呼叫当成一个SIP UA来处理。 - 媒体层面:FreeSWITCH要能终结WebRTC的SRTP/DTLS,把媒体转成内部呼叫用的RTP,再转给对端。这也是为什么很多人在FreeSWITCH上配完WebRTC却没有声音,问题往往出在DTLS-SRTP没有正确开启,编码没有协商对。
4.2 一个可用的Sofia Profile配置思路
以我做过的一个FreeSWITCH与浏览器SIP软电话对接的项目为例,关键不是去下载什么“WebRTC模块”,而是把sofia的profile、编解码、DTLS证书配到位。大致是这样:
xml复制<profile name="webrtc">
<!-- WebSocket监听端口,sip.js默认使用ws/wss连接 -->
<param name="ws-binding" value=":5066"/>
<param name="wss-binding" value=":7443"/>
<!-- WSS需要TLS证书,DTLS-SRTP也要用证书 -->
<param name="tls-cert-dir" value="$${certs_dir}"/>
<param name="tls-version" value="1.2"/>
<!-- 媒体地址,如果做NAT映射必须显式配置 -->
<param name="rtp-ip" value="192.0.2.10"/>
<param name="ext-rtp-ip" value="203.0.113.5"/>
<!-- 编解码:WebRTC的音频基本就是opus,视频是VP8/VP9/H264 -->
<param name="codec-prefs" value="OPUS,VP8"/>
<param name="inbound-codec-prefs" value="OPUS,VP8"/>
<param name="inbound-late-negotiation" value="true"/>
</profile>
这里有几个点经常让人踩坑:
- 浏览器对H264的支持是有条件限制的。Chrome在部分平台上能走H264硬件编码,Safari偏好H264;VP8的兼容性反而最好。如果一定要跟传统SIP终端互通,传统终端通常只有PCMA/PCMU,而WebRTC默认只有Opus,那你必须在FreeSWITCH上用
mod_com_g729或转码模块做编码转换,或者在SDP里同时带上PCMU,让网关来决定。 inbound-late-negotiation要设成true,让FreeSWITCH在收到呼叫时再确定媒体。- WSS的证书链必须是完整的,浏览器不接受自签证书,除非你手动信任。Debug的时候可以先走
ws://而不是wss://,但生产环境必须有合法的TLS证书。
4.3 另一条路线:mod_verto
FreeSWITCH还有一个自己的WebRTC信令协议叫Verto,它走WebSocket,媒体依然是WebRTC那套。如果你用FreeSWITCH自带的或Verto兼容的WebRTC客户端,信令层可以先不碰SIP,而是走Verto到FreeSWITCH,由FreeSWITCH内部再转成SIP呼叫。
不过我要提醒一句:如果只是“浏览器拉取FreeSWITCH上的会议流”或者“把浏览器桌面共享推进FreeSWITCH会议”,Verto是顺手的;但如果你要做的是标准SIP终端的注册、呼叫、CTI控制,那么直接走SIP over WebSocket加sip.js是更通用、生态更好的方式。
我自己最后选的是后者,因为后续要跟运营商SIP中继对接时,业务逻辑都是基于SIP的,没有必要绕道Verto。
4.4 调试互通问题的顺序建议
如果FreeSWITCH和WebRTC客户端通了但没画面没声音,我通常按这个顺序查:
- 看WSS链路是否通:浏览器控制台有没有
WebSocket connection failed。 - 看REGISTER是否成功:FreeSWITCH日志里有没有
Sofia::resend或auth challenge。 - 看呼叫是否建立:有没有
CHANNEL_CREATE。 - 看媒体协商结果:拨号计划里被叫方向用的编码是什么,SDP里最终选择的编码是什么。
- 看RTP是否双向流动:用
wireshark抓包,同时抓rtp和dtls;如果只有一端RTP包,说明SRTP协商或地址有问题。
别上来就改一堆参数。先确认信令通,再说媒体。
5. 弱网卡顿优化:核心不是调大缓冲,而是让发送速率主动逼近链路容量
“WebRTC弱网卡顿怎么优化”是搜索热词,也是面试和项目提测里最容易暴露问题的地方。我先给一个反直觉的结论:卡顿的主要根源,不是接收端的播放缓冲区太小,而是发送端在单位时间内塞进了超过链路容量的数据。链路装不下,路由器就开始丢包,接收端收不齐视频帧,画面自然就卡。
5.1 WebRTC自带拥塞控制,但你要给它正确的输入
WebRTC里的GCC(Google Congestion Control)一直在做一件事:根据RTT和丢包率,估算当前链路的可用带宽,然后告诉编码器“码率不要超过这个值”,同时告诉发送模块“该发多少包”。
但GCC不是万能的,它的判断依赖两个反馈来源:
- RTCP Receiver Report里的丢包率。
- Transport-CC反馈里的到达时间,用于延迟趋势判断。
如果网络抖动特别厉害,或者接收端的jitter buffer参数不合适,反馈本身就会失真。所以你看到很多WebRTC工程化方案里,真正做的不是去改GCC算法,而是给GCC提供去伪存真的反馈、调整接收端主动丢帧的策略、以及在关键帧上做冗余。
5.2 明确服务质量目标:互动优先还是画质优先
弱网优化之前,你要先定义什么叫“好”。我把通话类项目分成两类目标:
- 体验优先型(视频会议、连麦、在线课堂):要求延迟低、声音不断、画面不长时间冰冻。此时宁愿降低分辨率/帧率,也不能让数据在链路里积压。
- 内容优先型(大屏监控、远程协作、直播观看):可以容忍一定延迟,但画面要够清晰,细节不能糊。
这两类目标对应不同的调参方向。比如Chrome的RTCRtpSender.setParameters里有一个degradationPreference参数,maintain-framerate会优先保帧率、宁可降分辨率;maintain-resolution则优先保分辨率、宁可降帧率。选哪个取决于你的业务,而不是通用模板。
5.3 抗丢包三板斧:NACK、FEC、编码冗余
弱网丢包不可怕,可怕的是丢关键帧。WebRTC处理丢包的主要手段有三种,适用于不同场景。
- NACK重传:接收端发现自己缺了某个RTP包序号后,通过RTCP NACK告诉发送端“这个包丢了,请重发”。这种方式恢复准确,但代价是增加一个RTT的延迟。在RTT很低的网络中,NACK是首选。
- FEC冗余:发送端额外产生一些冗余包,接收端即便丢了部分包也能通过冗余包重建原始数据。代价是带宽占用上升。在丢包率较高但RTT也很高的网络里,FEC比NACK更可靠。
- 编码层冗余:音频里可以开启
RED,把一个音频包重复发送多次。视频里可以通过RFC 2198或结合ULP FEC做部分冗余。
具体到代码里,浏览器端你很难直接去开关NACK和FEC,因为它们由RTP Header扩展和SDP参数决定。但你可以影响它:下行网络差时,让SFU决定是否向推流端请求关键帧;上行网络差时,调低编码码率比单纯调缓冲池更有效。
5.4 Simulcast和多层流是大型弱网方案的解药
如果只有一个拉流端,那发送端只能给一条流。但当接收端网络参差不齐时,更好的办法是推流端发出多个质量的流,让SFU按需转发。这就是Simulcast的大致思路:同一路画面同时编码出高分辨率、中分辨率、低分辨率三个层次的RTP流,拉流端网络变差时,SFU自动把转发的流切到更低层,接收端的画质下降,但流畅性保住了。
启Simulcast在服务端需要配置,在浏览器端需要修改addTransceiver的sendEncodings参数,类似:
javascript复制pc.addTransceiver('video', {
direction: 'sendonly',
sendEncodings: [
{ rid: 'h', maxBitrate: 2_500_000, maxFramerate: 30 },
{ rid: 'm', maxBitrate: 1_000_000, maxFramerate: 30 },
{ rid: 'l', maxBitrate: 300_000, maxFramerate: 15 }
]
});
要注意,这个参数必须在addTransceiver时指定,后面再改经常不生效。如果服务端用的是SFU,SFU也要开启相应的Simulcast转发支持。
5.5 实测中的几个有效调参方向
基于实际项目经验,弱网卡顿的处理优先级我一般这样排:
- 确认弱网指标到底坏在哪里。只看“卡顿”这两个字没法定位,要看是丢包率高、RTT大还是jitter大。丢包率高走FEC和码率下调;RTT大不要死等NACK,要降码率;jitter大要调接收端缓冲策略。
- 给发送端设置合理码率上限。即使网络很好,也别让编码器跑到4Mbps,因为在线会议场景里,观众端往往在移动网络或者WIFI跨网段场景,一个大帧突发直接就能打爆链路。
- 开启音视频流的差异化策略。音频永远优先级最高。在弱网时优先保证音频,视频可以在关键帧上主动丢帧。
- 关键帧请求要克制。丢失I帧之后,接收端会发PLI请求关键帧,但如果网络还在恶化,立刻重发一个巨大的I帧只会加剧拥塞。成熟方案通常会在收到PLI后,先切到低分辨率低帧率发一个I帧,等链路恢复后再回到原质量。
- 在SFU侧做丢包和重传的优先级调度。不要一有NACK就重传所有包,优先重传音频包、离播放时间近的视频包,其他包放弃,因为重传回来也已经过了播放点。
我遇到过最典型的“调参失败”场景是:项目经理说弱网卡,开发就直接把码率上限调高了一倍,结果更卡。原因很简单,链路本来就只能承载500kbps,你把上限调到1Mbps,等于让发送端更努力地往一个堵死的管道里灌水。弱网优化的第一步永远是“让发送速率贴近链路容量”,而不是“提高发送上限”。
6. 能救命的自检手段和源码深入方向
6.1 chrome://webrtc-internals是默认排障入口
浏览器跑WebRTC,永远先开chrome://webrtc-internals,录一段问题视频,然后导出JSON交给后端一起看。这个页面里的关键字段这么看:
RTT:如果长期大于300ms,交互体验必然受损。Packets Lost:看的是累计值,要自己计算“一段时间内的增量”,如果每秒丢包增幅很大,说明链路已经拥塞。Frames Decoded和Frames Dropped:如果Frames Dropped在快速增长,说明接收端来不及解码,可能是CPU吃紧,或者到了播放时间点但对应的帧还没齐,只能丢。googTargetEncBitrate:这个值是远端拥塞控制给发送端的码率上限。如果它已经被压得很低,就说明链路预算不足,别再试图上调码率了。
6.2 本地模拟弱网,而不是等到线上被投诉
我建议每个WebRTC项目从第一天起就把弱网模拟列进测试计划。Linux上最常用的是tc命令,比如模拟丢包:
bash复制sudo tc qdisc add dev eth0 root netem loss 10%
改回正常状态就执行:
bash复制sudo tc qdisc del dev eth0 root
也可以模拟延迟和抖动:
bash复制sudo tc qdisc add dev eth0 root netem delay 100ms 20ms distribution normal
用这种方式把丢包、延迟、乱序拆开来测,你才能知道当前应用的瓶颈在哪里。很多人一上来就在Wi-Fi信号差的环境里测,结果问题一会儿是丢包一会儿是延迟,根本没法精确定位。
6.3 想看源码,从哪几个模块下手
WebRTC的源码确实比较庞大,但不建议无目标地去读。如果你想理解“流数据传输”的底层逻辑,我建议重点关注这几个模块:
modules/congestion_controller:拥塞控制的核心,尤其是GoogCcNetworkControl。modules/rtp_rtcp/source:RTP包的封装、NACK和RTCP反馈的处理。modules/video_coding:jitter buffer、帧丢失和关键帧逻辑。pc/peer_connection:协商状态机,理解offer/answer和ICE candidate的处理流程。
打开源码别从头读到尾,先找一个具体的现象,比如“丢包率高于5%时重传是怎么触发的”,从现象反查代码链路,效率会高很多。
6.4 一个实用的自检习惯
我建议你在信令服务里埋一套简单的质量日志:每次通话结束,把推流端和拉流端的getStats()结果上报到业务后端。长期收集这些数据之后,你会发现自己能很快判断“卡顿是因为哪一侧的网络劣化”,而不是靠用户描述去猜。
获取统计的快照可以参考这段逻辑:
javascript复制async function getStatsReport(pc) {
const stats = await pc.getStats();
const report = {};
stats.forEach(stat => {
if (stat.type === 'inbound-rtp' && stat.kind === 'video') {
report.videoPacketsLost = stat.packetsLost;
report.videoFramesDecoded = stat.framesDecoded;
report.videoFramesDropped = stat.framesDropped;
}
if (stat.type === 'candidate-pair' && stat.selected) {
report.currentRtt = stat.currentRoundTripTime;
}
});
return report;
}
上线前把这段逻辑跑起来,后续调优时你会感谢当年的自己。
我在多次项目里的体会是,WebRTC流传输的上手门槛其实不在“跑通”,而在于你能不能把你的业务目标翻译成一堆可调参数和质量指标。Demo谁都能跑通,但搞明白什么时候该上SFU、跟FreeSWITCH互通时要调哪些开关、网络变差时是先降清晰度还是先降帧率,这些判断力才决定一个WebRTC项目能不能真正落地。这篇文章里给的方向和命令都是我实际用过的,如果你现在正卡在某个具体环节,不妨先从抓取指标和看日志开始,把问题量化之后,你会发现解决方案往往自己就浮现出来了。
