一、废物利用的灵感

上两篇文章讲了直播架构选型和浏览器推流的坑,过程中我手里攒了几个“废品”:

  1. 浏览器摄像头采集的代码(本来想用于推流,结果码率被卡死)

  2. getDisplayMedia 屏幕共享的代码(同样因为码率问题,推流画质拉胯)

  3. 一个能跑通的 WebRTC 连接(虽然推流不行,但连接建立没问题)

扔了可惜,于是我冒出个想法:

不如把这些改成视频会议功能?

逻辑上好像完全说得通:

  • 直播是“一对多”,一个主播推流给所有人看

  • 会议是“多对多”,每个人都能发言,互相都能看到

  • getUserMedia 和 getDisplayMedia 本来就更适合会议场景(共享PPT、代码演示)

说干就干。

二、架构选择:P2P Mesh,简单直接

既然不是直播了,而是小团队内部开会,我第一反应就是最简单粗暴的方案——WebRTC P2P Mesh 组网

Mesh 是什么?

text

     用户A
     / | \
    /  |  \
   B   C   D
  / \ / \ / \
 E   F   G   H

每个参会者都和其余所有人建立一对一的 WebRTC 连接。A 把自己的音视频推给 B、C、D,同时接收 B、C、D 推过来的音视频。

为什么一开始选 Mesh?

  • 零服务器成本:不需要流媒体转发服务器,STUN/TURN 打洞就行

  • 逻辑简单:每个客户端维护 N-1 个 RTCPeerConnection,代码很好写

  • 延迟极低:数据直传,不经过任何中转

代码实现也不复杂:

javascript

// 伪代码:Mesh 模式下,每个新加入的成员都和所有人建立连接
function onNewParticipant(newPeerId) {
  const pc = new RTCPeerConnection(config);
  
  // 把自己的流加到连接里
  localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
  
  // 创建 offer,通过信令服务器发给对方
  pc.createOffer().then(offer => {
    pc.setLocalDescription(offer);
    signaling.send(newPeerId, { type: 'offer', sdp: offer.sdp });
  });
  
  // 收到对方的音视频
  pc.ontrack = (event) => {
    addRemoteVideo(newPeerId, event.streams[0]);
  };
}

两个人测试:完美。
三个人测试:没问题。
四个人的时候——

画面开始卡了,风扇开始转了。

三、Mesh 的带宽灾难

我以为写代码的时候已经理解了 Mesh 的问题,但真正跑起来,那个效果还是超出了预期。

算一笔账

假设4个人开会,每人推一路 720P 的视频,码率大约 2Mbps。

在 Mesh 模式下,每个人要处理:

方向 带宽计算 数值
上传 把自己的视频推给另外3个人 2Mbps × 3 = 6Mbps
下载 接收另外3个人的视频 2Mbps × 3 = 6Mbps
编解码 编码自己1路 + 解码对方3路 总共处理4路视频

4个人还能撑住。

6个人呢?

方向 带宽计算 数值
上传 推给另外5个人 2Mbps × 5 = 10Mbps
下载 接收另外5个人的视频 2Mbps × 5 = 10Mbps
编解码 编码1路 + 解码5路 总共处理6路视频

家用宽带的上行带宽通常在 20-50Mbps,10Mbps 还在范围内。

10个人呢?

方向 带宽计算 数值
上传 推给另外9个人 2Mbps × 9 = 18Mbps
下载 接收另外9个人的视频 2Mbps × 9 = 18Mbps
编解码 编码1路 + 解码9路 总共处理10路视频

部分用户的上行已经吃紧了。而且10路视频的解码,CPU 开始冒烟了。

30人呢?

方向 带宽计算 数值
上传 推给另外29个人 2Mbps × 29 = 58Mbps
下载 接收另外29个人的视频 2Mbps × 29 = 58Mbps
编解码 编码1路 + 解码29路 根本不现实

到这一步,别说带宽不够,光解码30路视频,大部分电脑就直接死机了。

这还没算屏幕共享

如果有人在分享屏幕,那更惨。屏幕共享的码率通常比摄像头高得多(静态内容还好,一旦有动画或者滚动,码率直接起飞)。

5个人开会,其中1个人分享屏幕,2Mbps摄像头 + 5Mbps屏幕共享 = 7Mbps上传,推给4个人就是 28Mbps

参会者的电脑:解码4路摄像头 + 1路屏幕共享,桌面端勉强扛得住,笔记本风扇已经开始起飞了。

四、为什么 Zoom、腾讯会议不用 Mesh?

这时候我突然意识到一个问题:Zoom 支持1000人同时在线的大型会议,按 Mesh 的算法,每个人需要推 999 份视频流。这根本不可能。

它们用的其实不是 Mesh,而是一种叫 SFU(Selective Forwarding Unit,选择性转发单元)的架构。

text

    用户A──┐
    用户B──┼──→ SFU服务器 ──→ 用户A、B、C、D...
    用户C──┤    (按需转发)
    用户D──┘

在 SFU 模式下:

  • 每个人只把自己的视频推1份给服务器

  • 服务器根据每个人的需求(比如只看发言人、只看固定几个人),选择性转发给每个人

  • 每个人只需要上传1份,下载也只接收自己需要的那几路

同样的30人会议,SFU模式下的带宽:

方向 带宽 说明
上传 2Mbps × 1 = 2Mbps 只推1份给服务器
下载 服务器转发来的几路 取决于你同时看几个人

从58Mbps降到2Mbps,这就是 SFU 的威力。

Mesh vs SFU 对比

维度 Mesh SFU
架构 端到端直连 服务器中转
服务器成本 几乎为零(只需信令) 需要中转服务器
上行带宽 N-1 倍 1 倍
下载带宽 N-1 倍 可控
编解码压力 解码 N-1 路 可控
延迟 极低(直连) 稍高(经服务器)
适用人数 ≤4人 几十到上千人

五、我把 Mesh 保留下来了,但加了人数限制

踩完坑之后,我没完全推倒重来。Mesh 有它的优点——简单、零成本、延迟极低——对于4人以内的小团队会议,其实完全够用。

于是我做了一个折中:

  1. 保留 Mesh 模式,但在页面上加了一个人数上限:最多6人

  2. 超过6人时,提示用户切换到直播模式(SRS转发),或者对接 SFU 方案

  3. 屏幕共享功能保留,但只在2-3人小会时推荐使用

代码里加了一个简单的检测:

javascript

const MESH_MAX_PARTICIPANTS = 6;

function onNewParticipant(peerId) {
  const currentCount = Object.keys(peers).length;
  
  if (currentCount >= MESH_MAX_PARTICIPANTS) {
    console.warn(`Mesh 模式最多支持 ${MESH_MAX_PARTICIPANTS} 人,当前 ${currentCount} 人,建议切换模式`);
    // 可以自动切换到仅接收模式,或者提示用户
    return;
  }
  
  // 正常建立 Mesh 连接...
}

六、总结:Mesh 的正确打开方式

这次折腾让我搞清楚了一件事:Mesh 不是不能用,而是要知道它的边界。

场景 推荐方案 原因
2-4人小会 Mesh 零成本,延迟最低
5-10人会议 SFU(自建或第三方) 带宽可控
10人以上 SFU 或直播模式 Mesh 根本不现实
大型会议/发布会 直播模式(SRS + RTMP/FLV) 一对多广播

技术选型这件事,永远没有绝对的“最好”,只有“在某个场景下最合适”。

我把屏幕共享的代码保下来没浪费,改成了小团队会议功能,顺便摸清了 Mesh 的极限在哪里——这个收获,比最初只想搞浏览器推流的时候大多了。


三篇文章串起来,刚好是一条完整的技术探索路线:

  1. 第一篇文章:直播架构选型,为什么用 SRS 而不是 WebRTC P2P

  2. 第二篇文章:浏览器推流踩坑,Windows 硬编码码率被卡的死死的

  3. 第三篇文章:把闲置的屏幕共享改成会议,踩了 Mesh 的带宽坑

从直播到会议,从推流到组网,这些坑踩了一遍,基本上把 WebRTC 在实际项目里的边界都摸清楚了。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐