11、多用户屏幕共享:SFU架构 vs MCU架构、Mediasoup简介、房间内广播屏幕流

好,我们进入第11章。前面几章我们一直在讲一对一屏幕共享,但真实场景哪有这么简单?一个会议室里,可能有5个人、10个人,甚至更多人同时在线。每个人可能都想分享自己的屏幕,或者看别人的屏幕。这时候,点对点的架构就撑不住了。

你想想看,如果10个人开会,每个人都要把自己的屏幕流发给其他9个人。那每个人的上行带宽就要承担9路视频流,下行带宽要接收9路。这还不算音频。我见过一个团队,用P2P mesh架构做远程设计评审,结果主讲人的电脑直接卡死,风扇狂转。嗯,这就是典型的架构选型失误。

所以,多用户场景下,我们必须引入服务器端架构。今天我们就来聊聊两种主流方案:SFU和MCU,以及一个非常流行的SFU实现——Mediasoup。

SFU vs MCU:两种思路,两种代价

先看一张图,帮你快速理解两者的区别。

SFU vs MCU 架构对比 SFU(选择性转发单元) 用户A 用户B 用户C 用户D SFU 服务器只转发,不解码 MCU(多点控制单元) 用户A 用户B 用户C 用户D MCU 服务器解码+混流,再编码发送

说白了,SFU就是个聪明的转发器。它收到每个用户的音视频流,然后原封不动地转发给房间里的其他人。服务器不做解码、不做混流,CPU消耗极低。每个用户的上行带宽只发一路流,下行带宽收N-1路流。

MCU就不一样了。它把所有用户的流都收上来,解码、混流、重新编码,然后合成一路流再发给每个人。服务器压力巨大,但客户端很轻松——只需要收一路流就行。

核心区别一句话:SFU是「转发」,MCU是「合成」。SFU省服务器,费客户端带宽;MCU费服务器,省客户端带宽。

我个人习惯,只要客户端设备不是特别老旧,优先选SFU。为什么?因为服务器成本才是大头。你想想看,一个MCU服务器能扛多少人?我见过一个项目,用MCU做30人会议,服务器CPU直接飙到90%。换成SFU,同样的机器扛200人都没问题。

Mediasoup简介:为什么我推荐它?

市面上SFU实现不少,比如Janus、Licode、mediasoup。我为什么偏爱Mediasoup?

  • 纯C++实现,性能极高——底层基于libwebrtc,没有多余封装
  • Node.js驱动——控制层用JS写,前端同学也能快速上手
  • 管道式架构——每个流都是一个「管道」,你可以灵活地做转码、录制、推流
  • 文档清晰——API设计得很直观,不像某些项目文档写得跟天书一样

我记得第一次用Mediasoup做原型,从零到跑通第一个Demo,只花了一个下午。嗯,当然,踩坑是难免的。我曾经因为没设置正确的dtlsCertificate,导致客户端一直连不上服务器,排查了整整两个小时。后来发现是证书路径写错了。这种低级错误,犯过一次就记住了。

房间内广播屏幕流:核心流程

好,我们来看具体怎么实现。多用户屏幕共享,本质上就是「一个用户发布屏幕流,其他用户订阅」。Mediasoup里,这个过程分为三步:

  1. 创建房间和Router——每个房间对应一个Router,Router管理所有Transport
  2. 用户加入房间,创建Transport——每个用户需要一对Transport(发送和接收)
  3. 发布和订阅——发布者创建Producer,订阅者创建Consumer

看一段核心代码,你就明白了:

// 服务端:创建房间和Router
const mediasoup = require('mediasoup');

async function createRoom() {
  const worker = await mediasoup.createWorker();
  const router = await worker.createRouter({
    mediaCodecs: [
      {
        kind: 'video',
        mimeType: 'video/VP8',
        clockRate: 90000,
        parameters: {}
      },
      {
        kind: 'audio',
        mimeType: 'audio/opus',
        clockRate: 48000,
        channels: 2
      }
    ]
  });
  return router;
}

// 服务端:用户创建WebRTC Transport
async function createTransport(router) {
  const transport = await router.createWebRtcTransport({
    listenIps: [{ ip: '0.0.0.0', announcedIp: 'your.public.ip' }],
    enableUdp: true,
    enableTcp: true,
    preferUdp: true,
    initialAvailableOutgoingBitrate: 1000000
  });
  return transport;
}

// 客户端:发布屏幕流
async function publishScreen(transport, stream) {
  const track = stream.getVideoTracks()[0];
  const producer = await transport.produce({
    track,
    encodings: [
      { maxBitrate: 2000000 },  // 2Mbps 主层
      { maxBitrate: 500000 }    // 500Kbps 低层(用于Simulcast)
    ],
    codecOptions: {
      videoGoogleStartBitrate: 1000
    }
  });
  return producer;
}

// 客户端:订阅屏幕流
async function subscribeScreen(transport, producerId) {
  const consumer = await transport.consume({
    producerId,
    rtpCapabilities: router.rtpCapabilities
  });
  // consumer.track 就是收到的视频轨道
  return consumer;
}

小提示:Mediasoup支持Simulcast(分层编码)。你可以让发布者同时发送多个分辨率的流,订阅者根据自身带宽选择合适的一层。比如手机端只看低分辨率,大屏看高清。这个功能在屏幕共享场景下特别实用。

避坑指南:我曾经踩过的三个坑

做多用户屏幕共享,有几个地方特别容易出问题。我直接说我的血泪史:

  • 端口不够用——Mediasoup每个Transport都需要一个UDP端口。我曾经在线上环境只开了10000-10100的端口范围,结果20个人同时开会,端口直接耗尽。建议至少开20000个端口,或者用portRange动态分配。
  • ICE重启导致断流——网络切换时,ICE会重新协商。如果处理不当,屏幕共享会中断。我的做法是在客户端监听transport.on('icestatechange'),状态变为disconnected时主动重建Transport。
  • 音频和屏幕流不同步——如果你同时共享屏幕和系统音频,记得用同一个MediaStream对象。分开创建的话,时间戳可能对不上。嗯,这个坑我帮别人排查过好几次。

广播逻辑:谁在看,就发给谁

最后聊一下「广播」的本质。在Mediasoup里,没有所谓的「广播API」。广播就是:当一个人发布屏幕流后,服务端通知房间里的其他人,然后每个人各自创建Consumer去订阅。

代码逻辑大概是这样的:

// 服务端:当有新的Producer创建时
producer.on('score', (score) => {
  console.log(`Producer ${producer.id} 质量评分:`, score);
});

// 通知其他用户
router.on('newproducer', (producer) => {
  // 遍历房间内其他用户的Transport
  for (const peer of room.peers) {
    if (peer.id !== producer.appData.peerId) {
      // 通知客户端:有人开始共享屏幕了
      peer.socket.emit('newScreenShare', {
        producerId: producer.id,
        peerId: producer.appData.peerId
      });
    }
  }
});

客户端收到通知后,调用subscribeScreen()方法即可。整个过程,服务器只负责转发和信令协调,不碰视频数据。这就是SFU的精髓。

注意:屏幕共享流通常比特率较高(1080p可能到3-5Mbps)。如果房间里有10个人同时共享屏幕,每个人的下行带宽可能要承受30-50Mbps。建议在客户端做限制,比如只订阅当前活跃发言人的屏幕流,或者让用户手动选择要看谁的屏幕。

好了,这一章的内容就到这里。多用户屏幕共享的核心就是选对架构、用好工具、处理好细节。Mediasoup是个好选择,但工具再好,也得看你怎么用。下一章我们会深入Mediasoup的管道模型,聊聊如何做录制和转码。


公众号:蓝海资料掘金营,微信deep3321