11、多用户屏幕共享:SFU架构 vs MCU架构、Mediasoup简介、房间内广播屏幕流
好,我们进入第11章。前面几章我们一直在讲一对一屏幕共享,但真实场景哪有这么简单?一个会议室里,可能有5个人、10个人,甚至更多人同时在线。每个人可能都想分享自己的屏幕,或者看别人的屏幕。这时候,点对点的架构就撑不住了。
你想想看,如果10个人开会,每个人都要把自己的屏幕流发给其他9个人。那每个人的上行带宽就要承担9路视频流,下行带宽要接收9路。这还不算音频。我见过一个团队,用P2P mesh架构做远程设计评审,结果主讲人的电脑直接卡死,风扇狂转。嗯,这就是典型的架构选型失误。
所以,多用户场景下,我们必须引入服务器端架构。今天我们就来聊聊两种主流方案:SFU和MCU,以及一个非常流行的SFU实现——Mediasoup。
SFU vs 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里,这个过程分为三步:
- 创建房间和Router——每个房间对应一个Router,Router管理所有Transport
- 用户加入房间,创建Transport——每个用户需要一对Transport(发送和接收)
- 发布和订阅——发布者创建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