在mediasoup服务器框架简单介绍文中大致介绍了一下webrtc服务器mediasoup的结构框架,但是作为webrtc客户端如何与服务器建立连接,客户端该怎么做以及服务端如何响应客户端的请求,在这里详细介绍。
一、客户端行为
之前的一年到现在做的一个互动会议的项目,是使用c++ webrtc库编写客户端,webrtc版本为M84。服务器使用了原始mediasoup,由于还处于开发阶段,暂时没有做任何二次开发。客户端与mediasoup中的node.js模块建立websocket连接,websocket使用了websocketpp-0.8.1库,嵌入到客户端代码中,主要作用就是与node.js进行信令交互,node.js负责房间的管理以及信令接收与发送处理。客户端是对mediasoup服务器模块的worker进程不可见的,worker进程只与其他worker进程和node.js进行进程间通讯。
客户端大致流程如下图:
客户端代码启动后,首先建立websocket实例,并与服务器建立连接,连接地址为wss://127.0.0.1:4443+/?roomId=1&peerId=adbd25k1 ,其中在mediasoup中wss端口固定4443,roomid自选,peerid为随机的,位数不定。连接如果成功,则代表我们客户端后续可以与之进行信令的交互,连接成功是系统正常运行的必要条件。
正常的P2P情况是我本地生成SDP信息,然后设置本地SDP信息,再发送至远端,远端处理后获取远端的SDP信息发送至本端,然后设置远程SDP信息的。多人互动mediasoup服务器与本端连接过程也会进行这一步,只不过在一开始我们就需要先获取到服务器所能支持的rtp参数(参数包括服务器支持的音频编码格式,视频编码格式,rtp扩展头等信息)。因此websocket连接成功的第一件事便是发送获取rtpcability信息的指令,指令为json格式字符串:
客户端请求服务端rtp参数
{"data":{},"id":1185,"method":"getRouterRtpCapabilities","request":true}
收到请求后服务器node.js返回的rtp参数
{"response":true,"id":41,"ok":true,"data":{"codecs":[{"kind":"audio","mimeType":"audio/opus","clockRate":48000,"channels":2,"rtcpFeedback":[{"type":"transport-cc","parameter":""}],"parameters":{},"preferredPayloadType":100},{"kind":"video","mimeType":"video/VP8","clockRate":90000,"rtcpFeedback":[{"type":"nack","parameter":""},{"type":"nack","parameter":"pli"},{"type":"ccm","parameter":"fir"},{"type":"goog-remb","parameter":""},{"type":"transport-cc","parameter":""}],"parameters":{"x-google-start-bitrate":1000},"preferredPayloadType":101},{"kind":"video","mimeType":"video/rtx","preferredPayloadType":102,"clockRate":90000,"parameters":{"apt":101},"rtcpFeedback":[]},{"kind":"video","mimeType":"video/VP9","clockRate":90000,"rtcpFeedback":[{"type":"nack","parameter":""},{"type":"nack","parameter":"pli"},{"type":"ccm","parameter":"fir"},{"type":"goog-remb","parameter":""},{"type":"transport-cc","parameter":""}],"parameters":{"profile-id":2,"x-google-start-bitrate":1000},"preferredPayloadType":103},{"kind":"video","mimeType":"video/rtx","preferredPayloadType":104,"clockRate":90000,"parameters":{"apt":103},"rtcpFeedback":[]},{"kind":"video","mimeType":"video/H264","clockRate":90000,"parameters":{"packetization-mode":1,"level-asymmetry-allowed":1,"profile-level-id":"4d0032","x-google-start-bitrate":1000},"rtcpFeedback":[{"type":"nack","parameter":""},{"type":"nack","parameter":"pli"},{"type":"ccm","parameter":"fir"},{"type":"goog-remb","parameter":""},{"type":"transport-cc","parameter":""}],"preferredPayloadType":105},{"kind":"video","mimeType":"video/rtx","preferredPayloadType":106,"clockRate":90000,"parameters":{"apt":105},"rtcpFeedback":[]},{"kind":"video","mimeType":"video/H264","clockRate":90000,"parameters":{"packetization-mode":1,"level-asymmetry-allowed":1,"profile-level-id":"42e01f","x-google-start-bitrate":1000},"rtcpFeedback":[{"type":"nack","parameter":""},{"type":"nack","parameter":"pli"},{"type":"ccm","parameter":"fir"},{"type":"goog-remb","parameter":""},{"type":"transport-cc","parameter":""}],"preferredPayloadType":107},{"kind":"video","mimeType":"video/rtx","preferredPayloadType":108,"clockRate":90000,"parameters":{"apt":107},"rtcpFeedback":[]}],"headerExtensions":[{"kind":"audio","uri":"urn:ietf:params:rtp-hdrext:sdes:mid","preferredId":1,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"video","uri":"urn:ietf:params:rtp-hdrext:sdes:mid","preferredId":1,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"video","uri":"urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id","preferredId":2,"preferredEncrypt":false,"direction":"recvonly"},{"kind":"video","uri":"urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id","preferredId":3,"preferredEncrypt":false,"direction":"recvonly"},{"kind":"audio","uri":"http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time","preferredId":4,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"video","uri":"http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time","preferredId":4,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"audio","uri":"http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01","preferredId":5,"preferredEncrypt":false,"direction":"recvonly"},{"kind":"video","uri":"http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01","preferredId":5,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"video","uri":"http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07","preferredId":6,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"video","uri":"urn:ietf:params:rtp-hdrext:framemarking","preferredId":7,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"audio","uri":"urn:ietf:params:rtp-hdrext:ssrc-audio-level","preferredId":10,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"video","uri":"urn:3gpp:video-orientation","preferredId":11,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"video","uri":"urn:ietf:params:rtp-hdrext:toffset","preferredId":12,"preferredEncrypt":false,"direction":"sendrecv"}]}}
id是随机random()生成的,method固定,request=true;服务器返回给本端的将是服务器所能支持的音视频rtp参数。本端拿到这个参数后,获取本地所能支持的rtp参数(即sdp信息),与服务器的参数对比,筛选出两者共同,共同的rtp参数即我本端和服务器端都可以编解码音视频的参数,如果服务器发送的音视频编码器不在这个共同的rtp参数范围,则无法正确解码。到此,我们通过服务器返回的rtp参数能够筛选出客户端可支持解码的rtp参数,但是服务器还无法知道我这个客户端到底支持哪些,所以之后我们会将本地客户端的rtp参数发送给服务器。
接下来获取服务器的ICE、ICE候选者、DTLS参数并创建本地发送传输,通过发送服务器创建createWebrtcTransport命令使得服务器worker进程创建对应的传输通道。
{"data":{"consuming":false,"forceTcp":false,"producing":true,"sctpCapabilities":""},"id":13416,"method":"createWebRtcTransport","request":true}
由于我们是创建了发送传输实例,目的是发送本端音视频媒体流到服务器,属于产生媒体流的一端(produce),因此命令中consuming=false,producing=true,forceTcp=false表是不支持TCP候选者。id=随机rondom产生,method固定,request=true属于请求命令,这个请求命令的目的是通知服务器端为接收我本端的媒体流创建相应的实例,该创建的UDP连接的创建,该创建的TCP连接创建,该创建的DTLS传输实例创建好等,这些实例在服务器端通称webrtcTransport实例。
服务器收到指令后,会返回服务器的ICE参数iceParameters、ICE候选者iceCandidates、DTLS参数dtlsParameters:
{"response":true,"id":4004,"ok":true,"data":{"id":"7353c883-a852-431c-8c83-089885cbb10a","iceParameters":{"iceLite":true,"password":"wmczw9u1xhw6xkab5f5stuzm2ulvq5zo","usernameFragment":"rpr6vopks8vh0rxq"},"iceCandidates":[{"foundation":"udpcandidate","ip":"172.16.52.185","port":47274,"priority":1076302079,"protocol":"udp","type":"host"}],"dtlsParameters":{"fingerprints":[{"algorithm":"sha-1","value":"81:D0:09:6A:81:54:50:CE:68:34:92:28:42:AD:B6:CE:C6:CD:2C:AE"},{"algorithm":"sha-224","value":"04:F3:C6:F4:02:A3:D0:F7:EB:E2:83:F3:64:5C:95:FE:74:0E:91:CB:91:1E:E5:AD:06:67:BC:76"},{"algorithm":"sha-256","value":"60:3D:22:F5:F6:42:15:0D:AF:F4:39:C1:DB:49:6A:48:9A:61:D5:FF:A3:06:4E:37:D5:E0:5B:E9:9D:09:81:09"},{"algorithm":"sha-384","value":"6D:E8:47:FA:D6:BC:44:CC:29:7D:7F:DB:64:7B:E9:0B:F7:2F:A3:E6:50:DE:D3:74:85:DD:A0:4A:D6:AB:E8:CA:07:01:D4:46:1A:CA:7B:1D:A7:90:BF:5B:01:50:4B:8D"},{"algorithm":"sha-512","value":"C7:ED:F5:4F:4D:3D:0E:8B:53:A5:CE:E5:58:54:7F:C6:60:3B:78:47:46:2D:24:35:46:F4:E7:69:6B:FB:C4:C0:EE:E0:7E:14:F6:6A:CF:F4:82:45:F5:D4:B5:EC:8C:35:62:74:9F:C8:70:DD:AB:55:F4:4D:51:FD:F0:C8:2F:3F"}],"role":"auto"}}}
其中iceLite=true表明服务器的iceLITE实现(当做为服务器时,有公网IP的情况下,申明我是LITE,就不需要connectivity check,由客户端来check)。根据服务器返回的参数我们在客户端也需要创建连接实例(就如webrtc中的P2P,首先需要创建peerconnection实例,在这个实例中同时实现音视频的发送与接收,但在基于mediasoup服务器的互动会议中,我们将接收和发送实现分离,即在同一个客户端,发送音视频会建立一个peerconnection实例,接收音视频也会建立一个peerconnection实例,两个实例之间没有交叉点)。
发送和接收的peerconnection实例都创建好后,对于发送音视频媒体流端需要创建音视频轨并添加音视频轨,然后通过api获取本地sdp,提取出dtls参数,本端dtls是作为服务器端的,mediasoup端的dtls作为客户端,创建信令命令发送服务区创建connectWebRtcTransport,主要目的是将本端的dtls参数发往服务器,具体服务器如何操作,看后面的服务器行为:
{"data":{"dtlsParameters":{"fingerprints":[{"algorithm":"sha-256","value":"83:3A:AC:24:6B:DD:DF:68:14:FE:1A:F7:85:45:E7:62:A4:DD:12:E8:75:7A:33:19:3A:4E:B5:A7:32:3F:8F:28"}],"role":"server"},"transportId":"2726b1d9-bafb-49f6-a418-885bb84ce762"},"id":32164,"method":"connectWebRtcTransport","request":true}
信令消息发送后设置本地SDP信息和远程SDP信息,然后将本端的rtp参数发送到服务器让服务器知道我这一端支持什么音视频编解码,rtp扩展头,拥塞控制策略等等。收到此信令消息服务器会创建produce实例用于媒体流接收,服务器收到本地参数后的处理看后面的服务器行为。信令消息:
{"data":{"appData":{},"kind":"audio","rtpParameters":{"codecs":[{"channels":2,"clockRate":48000,"mimeType":"audio/opus","parameters":{"minptime":10,"sprop-stereo":1,"usedtx":1,"useinbandfec":1},"payloadType":111,"rtcpFeedback":[{"parameter":"","type":"transport-cc"}]}],"encodings":[{"dtx":false,"ssrc":1561096710}],"headerExtensions":[{"encrypt":false,"id":4,"parameters":{},"uri":"urn:ietf:params:rtp-hdrext:sdes:mid"},{"encrypt":false,"id":2,"parameters":{},"uri":"http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"},{"encrypt":false,"id":3,"parameters":{},"uri":"http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"},{"encrypt":false,"id":1,"parameters":{},"uri":"urn:ietf:params:rtp-hdrext:ssrc-audio-level"}],"mid":"0","rtcp":{"cname":"","reducedSize":true}},"transportId":"0edfa440-1263-47c9-bd98-656b35cb43da"},"id":30561,"method":"produce","request":true}
至此客户端与服务器交互流程已经完成,客户端与服务器进行流媒体的相互传输前提已经建立,此时客户端的媒体流数据就可以发送到服务器了,后续就是服务器处理音视频拥塞和转发。
可能有人会很疑惑,我看webrtc中的P2P传输的例程peerconnectionclient源码,发现这里少了一步,没有收集本端候选者以及获取远端候选者并设置远端候选者。这里解析一下,mediasoup服务器是ice-lite模式,服务器本身不需要收集候选者,而是拿现成的,是通过外部获取映射后的公网IP或局域网ip,然后在在config.js中设置即可,我们使用的是webRtcTransport,因此在该标签下设置如下
webRtcTransportOptions :
{
listenIps :
[
{
ip : '**.**.**.**',//服务器本地映射ip
announcedIp : null //映射ip
}
],
initialAvailableOutgoingBitrate : 1000000,
minimumAvailableOutgoingBitrate : 600000,
maxSctpMessageSize : 262144,
// Additional options that are not part of WebRtcTransportOptions.
maxIncomingBitrate : 1500000
},
在我们创建发送传输通道时,服务器返回给我们ice,ice参数和dtls参数,这些参数都用来生成远程sdp信息的,因此远程sdp中多了一项ICE候选者,正常来说ice候选者也是sdp格式发送,只是和本地sdp信息分开,但是这里是放在一起的。发送通道设置远程sdp时同时应用了ICE候选者,接收通道设置本地SDP时同时应用了ICE候选者。远程SDP如下:
v=0
o=libmediasoupclient 10000 1 IN IP4 0.0.0.0
s=-
t=0 0
a=ice-lite
a=fingerprint:sha-512 6B:0C:C8:5B:C1:E2:E5:03:9F:BD:28:DB:B8:DC:DE:EC:52:CC:52:64:57:5F:4E:5F:04:39:DF:9B:C4:93:0D:71:5A:36:1F:FA:38:69:DD:13:A5:5C:12:EF:71:7F:D7:49:4C:FF:5C:E7:05:DD:FA:A3:B7:EC:C1:4A:E7:C9:63:B3
a=msid-semantic: WMS *
a=group:BUNDLE 0
m=audio 7 UDP/TLS/RTP/SAVPF 111
c=IN IP4 127.0.0.1
a=rtpmap:111 opus/48000/2
a=fmtp:111 stereo=1;usedtx=1
a=rtcp-fb:111 transport-cc
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=setup:active
a=mid:0
a=recvonly
a=ice-ufrag:14ijdfdwdmj66i9r
a=ice-pwd:2db8mc212ng7jjuw7201zpnuvslo0gfh
a=candidate:udpcandidate 1 udp 1076302079 172.16.52.185 40223 typ host
a=end-of-candidates
a=ice-options:renomination
a=rtcp-mux
a=rtcp-rsize
本端拿到ICE候选者后,与候选者ip地址创建UDP连接并开始发送ping(ping消息是一个stun请求)检测连通性,收到stun回应表明互通条件满足,然后等待服务器建立DTLS传输通道(因为此时服务器的dtls是作为客户端角色的),DTLS交换双方密钥(在webrtc中媒体流加密的密钥是对rtp流加密)后,将客户端支持的密钥发送到服务端,服务器支持的密钥发送到客户端,这样双方都可以正确的解密了,至此连接通道打通,音视频流可以正常互通。
二、服务器worker进程行为
服务器有两部分构成,node.js和c++ worker,node.js主要实现信令交互(相当于p2p中的信令服务器),房间管理。worker主要实现媒体流的处理。先从最外层说起,一个worker模块中主要包含两大块:管道通信模块和Router模块,管道通信模块是实现node.js和worker之间的消息交互,和worker与worker之间的信息交互;Router为一个房间中所有参会者媒体流的处理,一个房间对应一个Router,一个worker中可创建多个router。
客户端-node.js-worker之间的交互过程可以按照如下流程图:
在客户端与服务器创建websocket连接时会带入房间号roomid和peerid参数,如:wss://127.0.0.1:4443+/?roomId=1&peerId=adbd25k1 。node.js会创建room,并为Router分配随机32位RouteId,标识room与router对应关系,并告知worker创建Router实例,此时一个房间的外层结构框架已经完成。
连接成功后客户端会要求服务器返回支持的所有rtp参数,node.js会结合配置文件config.js整理一份rtp参数发送到客户端,这一步只在node.js中完成。接着客户端发送创建webrtcTransport通道,包括发送和接收两个。node.js接收后整理一下发送worker信令消息,如下:
{
"id":5,//随机ID,用于接收端识别正确的发送回应
"method":"router.createWebRtcTransport",//告知worker在router实例下创建webrtctransport实例
"internal":{
"routerId":"5ce4b1bd-7714-4a95-a09a-38c34e08945c",//webrtctransport实例所在的router实例下,根据对应id查找router
"transportId":"d0ac44ba-1e4e-4ca4-a3a4-d69ccec15a32"//node.js为webrtctransport实例分配的随机ID唯一标识
},
"data":{
"listenIps":[
{
"ip":"172.16.52.46"//ICE 候选者IP,端口由服务器分配然后返回给客户端,注意这个ip是在mediasoup的配置文件中配置
}
],
"enableUdp":true,//是否使用UDP候选者
"enableTcp":false,//是否使用TCP候选者
"preferUdp":false,//是否优先考虑UDP,对候选者优先级别有所以体现
"preferTcp":false,//是否优先考虑TCP,对候选者优先级别有所以体现
"initialAvailableOutgoingBitrate":1000000,//初始化服务器输出码率
"enableSctp":false,//是否支持SCTP数据通道
"numSctpStreams":{//如果支持SCTP数据通道,将应用这个参数
"OS":1024,
"MIS":1024
},
"maxSctpMessageSize":262144,//如果支持SCTP数据通道,将应用这个参数,如果支持,表示每个SCTP消息最大字节数
"sctpSendBufferSize":262144,//表示每个SCTP发送最大字节数
"isDataChannel":true //是否支持数据通道
}
}
在webrtcTransport实例创建过程,会进行一些实例的创建,可以看下图,根据配置创建候选者,并创建其UDP或TCP的socket服务端,用与监听客户端的联通检查、ping消息、音视频媒体数据流RTP、音视频媒体控制流RTCP,DTLS消息。同时也创建ICE服务和DTLS传输实例。最后将创建好的ICE参数、ICE候选者和DTLS参数返回给客户端。
客户端接着希望与服务端worker的webrtcTransport建立连接通道,发送客户但的dlts到服务器,服务器接收到connectWebRtcTransport后提取客户端的dtls参数,并将其设置到dtls实例中,至此C-S双方都获取到了互相的dtls参数。对于客户端发送媒体流时,其dtls的角色为dtls_server,mediasoup作为媒体流接收端来说dtls角色为dtls_client;对于客户端接收媒体流时,其dtls的角色为dtls_client,mediasoup作为媒体流发送端来说dtls角色为dtls_server。dtls_client总是主动发起连接的。此时服务器的dtls还会尝试去dtls握手,但是肯定是失败的,因为客户端还没有走到设置远程sdp的那一步,无法获悉服务端的候选者对,无法与服务器建立传输通道,因此服务器端的ice服务还是处于断开状态,只有ICE服务状态变为连接状态,才可以尝试dtls握手,dtls也是建立在ice通道上的。
Route实例的框架如下图所示:
上文提到过一个Route对应一个房间,每个房间可能有很多的参会者,每个参会者都会建立发送传输和接收传输,即图中的webRtcTransport实例,对于发送传输,每个参会者都会将音视频媒体流发送到服务器,服务器为每个流创建一个produce用于接收数据,如音频流会创建produce,视频也会创建produce,两者分开;每个参会者都会接收来自服务器的音视频媒体流,服务器为每个流创建一个consumer用于转发数据,如音频流会创建consumer,视频也会创建consumer,两者分开。所以在Route里面会有多个发送和接收实例,每个发送实例中有至少一个consumer实例,每个接收实例中至少一个produce实例。
客户端发送完connectWebRtcTransport后,告知服务器产生produce实例,用于准备接收客户端流媒体数据。服务器收到的信令消息如下。服务器获取到rtp参数后,创建produce实例,并保存客户端的rtp参数,需要根据客户端rtp参数中rtcpFeedback中type指定的拥塞控制策略建立拥塞控制服务器实例(对于接收媒体流端为拥塞控制server)来控制rtp流,如下消息中为transport-cc,因此创建tcc,如果为remb,则创建goog-remb,接收端的拥塞控制主要是向发送端发送RTPFB的RTCP报文,具体如何实现,在mediasoup服务器转发流媒体数据及发送拥塞控制中详细分析。
{
"id":12,
"method":"transport.produce",
"internal":{
"routerId":"2342606c-de05-4354-a822-edb55f8e2345",//produce实例所在的router实例ID
"transportId":"73fcd90d-1086-4244-aacd-84211da7f775",//produce实例所在的transport实例ID
"producerId":"1b5b2f10-2d71-4cc4-918e-348fe9ac4c8a"//node.js为worker分配的随机produce实例ID
},
"data":{
"kind":"audio",//音频类型
"rtpParameters":{//客户端rtp参数
"codecs":[
{
"channels":2,
"clockRate":48000,
"mimeType":"audio/opus",
"parameters":{
"minptime":10,
"sprop-stereo":1,
"usedtx":1,
"useinbandfec":1
},
"payloadType":111,
"rtcpFeedback":[
{
"parameter":"",
"type":"transport-cc"
}
]
}
],
"encodings":[
{
"dtx":false,
"ssrc":1593891542
}
],
"headerExtensions":[
{
"encrypt":false,
"id":4,
"parameters":{
},
"uri":"urn:ietf:params:rtp-hdrext:sdes:mid"
},
{
"encrypt":false,
"id":2,
"parameters":{
},
"uri":"http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"
},
{
"encrypt":false,
"id":3,
"parameters":{
},
"uri":"http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"
},
{
"encrypt":false,
"id":1,
"parameters":{
},
"uri":"urn:ietf:params:rtp-hdrext:ssrc-audio-level"
}
],
"mid":"0",
"rtcp":{
"cname":"6daec41b",
"reducedSize":true
}
},
"rtpMapping":{
"codecs":[
{
"payloadType":111,
"mappedPayloadType":100
}
],
"encodings":[
{
"mappedSsrc":859353967,
"ssrc":1593891542
}
]
},
"paused":false
}
}
至此,客户端与服务器端建立连接的信令交互已经完成,但是这里没有体现出来数据怎么能够到服务器。在客户端希望服务器创建接收我的媒体流时,会发送produce消息给服务器,同时设置本地SDP,和远程SDP,其中远程SDP中包含了服务器返回的ICE候选者IP和端口对。客户端在设置远程SDP信息后,应用其中的ICE候选者,对候选者Ping进行连通性检测,这个Ping消息就是STUN请求消息,服务器收到STUN请求,正确且可以解析后,发送stun成功回应,ICE服务状态变为connected,然后再一次进行DTLS握手,握手成功后,DTLS交换密钥,DTLS状态变为connected,一旦DTLS状态变为connected,客户端和服务器的传输通道彻底打通,客户端的数据就可以无阻碍到达服务器,数据到达服务器后,转发到所有的consumer,具体细节在mediasoup服务器接收流媒体数据及接收拥塞控制和mediasoup服务器转发流媒体数据及发送拥塞控制中已详细介绍。