mediasoup 是一个多用于多端视频会议系统的 SFU,它主要用来做各个端点之间的媒体数据等的转发。mediasoup 本身主要围绕媒体数据转发来构建。
这里先通过 mediasoup 的架构图,简单看一下 mediasoup 这个 SFU 建立的主要概念和抽象,如 Router、Transport、Producer、Consumer、DataProducer 和 DataConsumer 等,以及它们在 mediasoup 的媒体数据转发系统中的角色和作用:
1640916356184.jpg如上图,描述了 mediasoup 转发服务的主要架构。在 mediasoup 媒体数据转发系统的服务端,Router 是核心,它完成媒体数据的转发。Producer/DataProducer 抽象数据提供者,Consumer/DataConsumer 抽象数据消费者,Transport 抽象 WebRTC 数据传输。
mediasoup 客户端库,包括用于 JavaScript 的 mediasoup-client,和用于 C++ 的 libmediasoupclient,建立了一些抽象用来支持服务端的媒体数据转发。客户端的部分抽象的名称可能与它对应的服务端中的抽象名称相同,但含义有点不一样的。比如 Producer,在服务器端,这个角色拿到媒体数据给到 Router,但在客户端,它表示对 WebRTC 的媒体数据源的封装。客户端库 mediasoup-client 和 libmediasoupclient 中所有这些抽象的含义可以参考 API 文档,下文也会对这些抽象做更详细的说明。
有了客户端库和 mediasoup 服务,还不足以建立完整的多方视频会议场景,这还需要信令协议的协助。对于 mediasoup,需要信令协议在适当的时候协调 mediasoup SFU 服务器完成媒体转发所需要的服务端对象的创建,如 router、producer 和 consumer 等,资源的分配,和链路的打通,并协调客户端与服务端建立连接,发送接收数据,及数据收发控制,协调客户端和服务器之间交换媒体相关参数等。
mediasoup 本身不提供任何信令协议来帮助客户端和服务器进行通信。信令的传递取决于应用程序,它们可以使用 WebSocket、HTTP 或其它通信方式进行通信,并在客户端和服务器之间交换 mediasoup 相关的参数、请求/响应和通知。在大多数情况下,服务端可能需要主动向客户端递送消息或事件通知,则客户端和服务器的这种通信必须是双向的,因此通常需要全双工的通道。但是,应用程序可以服用相同的通道进行非 mediasoup 相关的消息交换 ( 例如身份验证过程、聊天消息、文件传输和任何应用程序希望实现的内容)。
前面说 mediasoup 本身不提供任何信令协议,其实不太准确。在 mediasoup v2 的时候,还是有信令协议的,具体内容如 mediasoup protocol 和 MEDIASOUP_PROTOCOL.md 的说明。但在最新的 v3 版中,已经没有这部分了。信令协议需要应用系统自己实现。
信令协议实现的示例可以参考 mediasoup-demo 的 server 的
mediasoup-demo/server/server.js
。
在客户端,Device 表示连接到 mediasoup 以发送/接收媒体的端点,可以认为它是整个应用程序的中心控制器,它协调和控制整个操作过程。Device 也是客户端应用程序的入口点。此外,mediasoup 的客户端库还建立了 Handler、Consumer、DataConsumer、Producer 和 DataProducer 等抽象,所有这些抽象的关系大致如下图:
mediasoup 客户端对象其中绿色的框中的组件是 WebRTC 提供的接口,蓝色框中的组件是对 WebRTC 接口的封装,黄色框中的组件是 mediasoup 客户端建立的抽象,红色框中的组件是对相应 mediasoup 接口的实现,Device 是各个组件的总控制器。
假设我们的 JavaScript 或 C++ 客户端应用程序初始化了一个 mediasoup-client Device 或一个 libmediasoupclient Device 对象,连接一个 mediasoup Router (已经在服务器中创建)并基于 WebRTC 发送和接收媒体数据。
注意:mediasoup-broadcaster-demo 中的 libmediasoupclient 客户端演示 broadcaster 程序不创建房间,它只连接已经创建好的房间 router。开发者可以将 router 想象为房间。
注意:这里创建 mediasoup Router 的服务端应用接口,如 mediasoup-demo 的 server 的
mediasoup-demo/server/server.js
实现的 websocket 接口所示:async function runProtooWebSocketServer() { logger.info('running protoo WebSocketServer...'); // Create the protoo WebSocket server. protooWebSocketServer = new protoo.WebSocketServer(httpsServer, { maxReceivedFrameSize : 960000, // 960 KBytes. maxReceivedMessageSize : 960000, fragmentOutgoingMessages : true, fragmentationThreshold : 960000 }); // Handle connections from clients. protooWebSocketServer.on('connectionrequest', (info, accept, reject) => { // The client indicates the roomId and peerId in the URL query. const u = url.parse(info.request.url, true); const roomId = u.query['roomId']; const peerId = u.query['peerId']; if (!roomId || !peerId) { reject(400, 'Connection request without roomId and/or peerId'); return; } logger.info( 'protoo connection request [roomId:%s, peerId:%s, address:%s, origin:%s]', roomId, peerId, info.socket.remoteAddress, info.origin); // Serialize this code into the queue to avoid that two peers connecting at // the same time with the same roomId create two separate rooms with same // roomId. queue.push(async () => { const room = await getOrCreateRoom({ roomId }); // Accept the protoo WebSocket connection. const protooWebSocketTransport = accept(); room.handleProtooConnection({ peerId, protooWebSocketTransport }); }) .catch((error) => { logger.error('room creation or room joining failed:%o', error); reject(error); }); }); }
这个 websocket 接口的请求者传入自己的 ID (peerId) 和房间 ID (roomId) 请求创建房间,这个接口则调用相同 JavaScript 文件中的
getOrCreateRoom({ roomId })
方法创建房间:/** * Get next mediasoup Worker. */ function getMediasoupWorker() { const worker = mediasoupWorkers[nextMediasoupWorkerIdx]; if (++nextMediasoupWorkerIdx === mediasoupWorkers.length) nextMediasoupWorkerIdx = 0; return worker; } /** * Get a Room instance (or create one if it does not exist). */ async function getOrCreateRoom({ roomId }) { let room = rooms.get(roomId); // If the Room does not exist create a new one. if (!room) { logger.info('creating a new Room [roomId:%s]', roomId); const mediasoupWorker = getMediasoupWorker(); room = await Room.create({ mediasoupWorker, roomId }); rooms.set(roomId, room); room.on('close', () => rooms.delete(roomId)); } return room; }
mediasoup-client (客户端 JavaScript 库) 和 libmediasoupclient (基于 libwebrtc 的 C++ 库) 都生成适用于 mediasoup 的 RTP 参数,这简化了客户端应用程序的开发。
mediasoup 使用的 RTP 参数 主要是以 JSON 描述的,但 WebRTC 中对这些参数的描述则主要是用 SDP 来完成,因而 libmediasoupclient 库的许多代码都在处理 SDP 和 JSON 格式之间的相互转换。这里提到的 “生成适用于 mediasoup 的 RTP 参数” 主要是指从 WebRTC 拿到 SDP 描述的参数,然后转为 JSON 的描述。
信令和 Peers
应用程序可以使用 WebSocket,并将每个经过认证的 WebSocket 连接与一个 “peer” 关联。
注意 mediasoup 中本身并没有 “peers”。然而,应用程序可能希望定义 “peers”,这可以标识并关联一个特定的用户账号与WebSocket 连接、metadata、及一系列 mediasoup transports、producers、consumers、data producers 和 data consumers。
设备加载
客户端应用程序通过给 device 提供服务端 mediasoup router 的 RTP capabilities 加载它的 mediasoup device。参考 device.load()。
对于纯粹的媒体数据接收端,设备加载是客户端完成的第一个主要动作。
这里的服务端 mediasoup router 的 RTP capabilities 需要通过信令协议从 mediasoup 服务器端获取。如对于 mediasoup-demo 的 server 应用,客户端需要向服务器端发送 GET 请求,服务器端返回 JSON 格式描述的 RTP capabilities 响应。HTTP url path 为 /rooms/:roomId
,如对于房间名为 broadcaster
的房间,为 /rooms/broadcaster
:
auto r = cpr::GetAsync(cpr::Url{baseUrl}, cpr::VerifySsl{verifySsl}).get();
if (r.status_code != 200) {
std::cerr << "[ERROR] unable to retrieve room info"
<< " [status code:" << r.status_code << ", body:\"" << r.text
<< "\"]" << std::endl;
return 1;
} else {
std::cout << "[INFO] found room " << envRoomId << std::endl;
}
auto response = nlohmann::json::parse(r.text);
响应为一个长长的 JSON 字符串。
mediasoup-demo 的 server 的 mediasoup-demo/server/server.js
对于这个请求的实现如下:
/**
* API GET resource that returns the mediasoup Router RTP capabilities of
* the room.
*/
expressApp.get(
'/rooms/:roomId', (req, res) =>
{
const data = req.room.getRouterRtpCapabilities();
res.status(200).json(data);
});
客户端库中,设备加载实现的大体流程如下:
- 获取本地的 RTP capabilities。
- 根据本地的 RTP capabilities 和传进来的服务器端 router 的 RTP capabilities 生成扩展的 RTP capabilities。
- 根据扩展的 RTP capabilities 生成接收的 RTP capabilities 和 SCTP capabilities。
在这个上下文中,capabilities 指的主要是端点支持的音视频编解码器配置、RTP 扩展和 RTCP 反馈等。
创建 Transports
mediasoup-client 和 libmediasoupclient 都需要将 WebRTC 传输的发送和接收分开。通常客户端应用程序会提前创建这些 transports,甚至在想要发送或接收媒体数据之前。即对于发送和接收,分别创建 WebRTC 的 PeerConnectionInterface 连接。
对于发送媒体数据:
- 必须首先在 mediasoup router 中创建 WebRTC transport: router.createWebRtcTransport()。
- 然后同样地在客户端应用程序中创建:device.createSendTransport()。
- 客户端应用程序必须订阅本地 transport 中的 “connect” 和 “produce” 事件。
以 mediasoup-broadcaster-demo 和 mediasoup-demo 为例来看创建 Transports 的过程。
对于 mediasoup-demo 的 server 的 mediasoup-demo/server/server.js
,在创建 transport 之前,还需要先在服务器中创建 Broadcaster,POST 请求为:
json body =
{
{ "id", this->id },
{ "displayName", "broadcaster" },
{ "device",
{
{ "name", "libmediasoupclient" },
{ "version", mediasoupclient::Version() }
}
},
{ "rtpCapabilities", this->device.GetRtpCapabilities() }
};
/* clang-format on */
auto url = baseUrl + "/broadcasters";
auto r = cpr::PostAsync(cpr::Url{url}, cpr::Body{body.dump()},
cpr::Header{{"Content-Type", "application/json"}},
cpr::VerifySsl{verifySsl})
.get();
其中 id 为本地生成的一个随机字符串。
响应为:
{
"peers":[
{
"id":"ej8ogujz",
"displayName":"Elgyem",
"device":{
"flag":"safari",
"name":"Safari",
"version":"14.1"
},
"producers":[
{
"id":"87230aeb-027e-4204-99eb-080cd4972bb0",
"kind":"audio"
},
{
"id":"66c62c26-7101-43b2-b82c-cdf537b8d9ed",
"kind":"video"
}
]
}
]
}
响应中主要包含了相同房间内,其它 peer 的信息。这里看到了一个新的概念 Broadcaster。Broadcaster 和上面的 Room 都是 mediasoup-demo 的概念。Room 中可以有多个 broadcaster 和 router。Broadcaster 表示一个主播,或 mediasoup 媒体数据转发中一个可以收发媒体数据的客户端端点,用来管理对应端点的 transports、consumer、producer、dataProducer 和 dataConsumer。但 mediasoup-demo 中没有观众 Audience 的概念。在房间中创建了 Broadcaster 之后创建 transports:
void Broadcaster::Start(const std::string& baseUrl,
bool enableAudio,
bool useSimulcast,
const json& routerRtpCapabilities,
bool verifySsl) {
std::cout << "[INFO] Broadcaster::Start()" << std::endl;
this->baseUrl = baseUrl;
this->verifySsl = verifySsl;
// Load the device.
this->device.Load(routerRtpCapabilities);
std::cout << "[INFO] creating Broadcaster..." << std::endl;
/* clang-format off */
json body =
{
{ "id", this->id },
{ "displayName", "broadcaster" },
{ "device",
{
{ "name", "libmediasoupclient" },
{ "version", mediasoupclient::Version() }
}
},
{ "rtpCapabilities", this->device.GetRtpCapabilities() }
};
/* clang-format on */
auto r = cpr::PostAsync(cpr::Url{this->baseUrl + "/broadcasters"},
cpr::Body{body.dump()},
cpr::Header{{"Content-Type", "application/json"}},
cpr::VerifySsl{verifySsl})
.get();
if (r.status_code != 200) {
std::cerr << "[ERROR] unable to create Broadcaster"
<< " [status code:" << r.status_code << ", body:\"" << r.text
<< "\"]" << std::endl;
return;
}
this->CreateSendTransport(enableAudio, useSimulcast);
this->CreateRecvTransport();
}
在 mediasoup router 中为 broadcaster 创建 WebRTC transport 通过如下 HTTP 请求完成:
json sctpCapabilities = this->device.GetSctpCapabilities();
/* clang-format off */
json body =
{
{ "type", "webrtc" },
{ "rtcpMux", true },
{ "sctpCapabilities", sctpCapabilities }
};
/* clang-format on */
auto url = baseUrl + "/broadcasters/" + id + "/transports";
auto r = cpr::PostAsync(cpr::Url{url}, cpr::Body{body.dump()},
cpr::Header{{"Content-Type", "application/json"}},
cpr::VerifySsl{verifySsl})
.get();
这个请求的响应为:
{
"id":"6eae5aae-3ae9-4545-a146-466b28e05da7",
"iceParameters":{
"iceLite":true,
"password":"g08jh0b528i0fshqld1cmdgijhzhstuz",
"usernameFragment":"v77q4zq05bhni7c1"
},
"iceCandidates":[
{
"foundation":"udpcandidate",
"ip":"192.168.217.129",
"port":40065,
"priority":1076302079,
"protocol":"udp",
"type":"host"
}
],
"dtlsParameters":{
"fingerprints":[
{
"algorithm":"sha-1",
"value":"5F:2D:8A:74:CD:95:65:3C:4B:10:27:1A:01:BA:CE:F7:0B:23:B9:AE"
},
{
"algorithm":"sha-224",
"value":"9C:19:4F:40:43:A9:AE:DD:01:00:7A:98:0C:5D:26:99:BD:9E:FB:A0:4F:EA:FB:0C:39:D2:2B:BD"
},
{
"algorithm":"sha-256",
"value":"D8:FD:D9:5B:9C:37:2A:4C:F7:99:D4:35:F2:90:7C:9E:D8:1A:74:10:B3:33:B4:71:B7:22:8F:C5:A5:59:FF:BD"
},
{
"algorithm":"sha-384",
"value":"B9:2B:D5:6C:60:0F:B0:A0:E3:6E:57:7D:02:91:52:AE:75:D7:3F:E1:34:83:45:39:DA:53:93:09:ED:53:6C:A9:01:1E:20:16:06:C3:48:40:07:9B:A5:6C:B3:E1:81:A9"
},
{
"algorithm":"sha-512",
"value":"46:F6:77:11:ED:ED:80:EA:97:EA:36:FF:CD:4B:E1:C0:36:09:ED:F4:E0:B8:56:F0:8D:FB:9C:12:AF:A3:86:05:82:C0:F8:B9:CA:E6:7D:62:5C:72:5F:10:23:F5:66:27:04:A5:BA:F4:63:D9:F5:42:D6:22:0C:86:51:43:1D:B4"
}
],
"role":"auto"
},
"sctpParameters":{
"MIS":1024,
"OS":1024,
"isDataChannel":true,
"maxMessageSize":262144,
"port":5000,
"sctpBufferedAmount":0,
"sendBufferSize":262144
}
}
创建 transport 的请求返回 media soup 服务器中对应的 transport 的连接参数。在 router 中创建 transport 对于客户端创建 transport 只是万里长征第一步。客户端创建 SendTransport 的完整过程如下:
- 如上所述,发送请求,让服务端为 broadcaster 创建 send transport,获得为这个 transport 分配的 ID 以及连接参数,包括 ICE 参数,ICE candidates,SCTP 参数和 DTLS 参数。
- 创建
SendTransport
对象,这个过程中还会一连串创建多个对象:SendTransport
->SendHandler
->Handler
->PeerConnection
->webrtc::PeerConnectionInterface
。 - 如果要发送音频,会创建 audio 的 Track,并通过这样一个调用过程将 audio 的 track 添加给
webrtc::PeerConnectionInterface
:SendTransport::Produce()
->SendHandler::Send()
->PeerConnection::AddTransceiver()
->webrtc::PeerConnectionInterface::AddTransceiver()
。SendTransport
还会基于 audio 的 track 创建 Producer。
(1). 在SendHandler::Send()
中,将 audio 的 track 添加个webrtc::PeerConnectionInterface
之后,会创建 Offer,创建 Offer 生成的 SDP 设置为 PeerConnection 的 local description。此后根据这个 Offer 的内容,和前面的步骤中从服务器获取的 mediasoup 服务器 capabilities 和 连接参数,在本地生成 Answer,并设置这个 Answer 为 PeerConnection 的 remote description。 - 如果要发送视频,会创建 video 的 Track,通过与音频类似的过程,将 video 的 track 添加给
webrtc::PeerConnectionInterface
,并基于 video 的 track 创建 Producer。如果既要发送音频,也要发送视频,则会对 peer connection 分别为音频和视频各执行一次(创建 Offer,设置 local description,创建 Answer,设置 remote description)的过程。 - 为 transport 创建 DataProducer。
对于接收媒体数据:
- WebRTC transport 必须首先在 mediasoup router 中创建: router.createWebRtcTransport()。
- 然后重复地在客户端应用程序中创建:device.createRecvTransport()。
- 客户端应用程序必须订阅本地 transport 中的 “connect” 和 “produce” 事件。
为接收媒体向信令服务发送的请求及请求的响应与上面发送媒体数据时的完全相同,这里不再赘述。客户端创建 SendTransport 的完整过程如下:
- 如上所述,发送请求,让服务端为 broadcaster 创建 transport,获得为这个 transport 分配的 ID 以及连接参数,包括 ICE 参数,ICE candidates,SCTP 参数和 DTLS 参数。
- 创建
RecvTransport
对象,这个过程中还会一连串创建多个对象:RecvTransport
->RecvHandler
->Handler
->PeerConnection
->webrtc::PeerConnectionInterface
。 - 创建 Consumer。
如果在这些 transports 中需要使用 SCTP (即 WebRTC 中的 DataChannel),必须在其中启用 enableSctp (使用适当的 numSctpStreams) 和其他 SCTP 相关设置。
生产媒体数据
一旦创建了 send transport,客户端应用程序就可以在其上生成多个音频和视频 tracks。
- 应用程序获得一个 track (例如,通过使用 navigator.mediaDevices.getUserMedia() API)。
- 它在本地 send transport 中调用 transport.produce()。
- 如果这是对 transport.produce() 的第一次调用,则 transport 将发出 “connect” 事件。
- transport 将发出 “produce” 事件,因此应用程序将把事件参数传递给服务器,并在服务器端创建一个 Producer 实例。
- 最后,transport.produce() 将在客户端使用 Producer 实例进行解析。
这个过程还可以参考上面所述 客户端创建 SendTransport 的完整过程 的第 3 步和 第 4 步。
这里的把事件参数传递给服务器,对应于 mediasoup-demo 的 server 的 mediasoup-demo/server/server.js
的连接 send transport 请求:
/* clang-format off */
json body =
{
{ "dtlsParameters", dtlsParameters }
};
/* clang-format on */
auto url = baseUrl + "/broadcasters/" + this->id + "/transports/" +
sendTransport->GetId() + "/connect";
std::cout << "Connect send transport url: " << url << std::endl;
auto r = cpr::PostAsync(cpr::Url{url}, cpr::Body{body.dump()},
cpr::Header{{"Content-Type", "application/json"}},
cpr::VerifySsl{verifySsl})
.get();
在本地 send transport 中调用 transport.produce() 时发出请求:
#0 Broadcaster::OnConnectSendTransport (this=0x3d440000c280, dtlsParameters=...) at ~/mediasoup-broadcaster-demo/src/Broadcaster.cpp:58
#1 0x0000555555655e86 in Broadcaster::OnConnect (this=0x7fffffffdbd0, transport=0x3d4400031180, dtlsParameters=...)
at ~/mediasoup-broadcaster-demo/src/Broadcaster.cpp:44
#2 0x00005555576d7d91 in mediasoupclient::Transport::OnConnect (this=0x3d4400031180, dtlsParameters=...)
at ~/mediasoup-broadcaster-demo/build/_deps/mediasoupclient-src/src/Transport.cpp:106
#3 0x00005555576bab97 in mediasoupclient::Handler::SetupTransport (this=0x3d44000bd280, localDtlsRole="server", localSdpObject=...)
at ~/mediasoup-broadcaster-demo/build/_deps/mediasoupclient-src/src/Handler.cpp:145
#4 0x00005555576bb6b0 in mediasoupclient::SendHandler::Send (this=0x3d44000bd280, track=0x3d4400085fc0, encodings=0x7fffffffcff0, codecOptions=0x7fffffffd1e0,
codec=0x0) at ~/mediasoup-broadcaster-demo/build/_deps/mediasoupclient-src/src/Handler.cpp:232
#5 0x00005555576d8f70 in mediasoupclient::SendTransport::Produce (this=0x3d4400031180, producerListener=0x7fffffffdbe0, track=0x3d4400085fc0, encodings=0x0,
codecOptions=0x7fffffffd1e0, codec=0x0, appData=...)
at ~/mediasoup-broadcaster-demo/build/_deps/mediasoupclient-src/src/Transport.cpp:220
#6 0x000055555565ae46 in Broadcaster::CreateSendTransport (this=0x7fffffffdbd0, enableAudio=true, useSimulcast=true)
at ~/mediasoup-broadcaster-demo/src/Broadcaster.cpp:420
#7 0x000055555565901e in Broadcaster::Start (this=0x7fffffffdbd0, baseUrl="https://192.168.217.129:4443/rooms/broadcaster", enableAudio=true, useSimulcast=true,
routerRtpCapabilities=..., verifySsl=false) at ~/mediasoup-broadcaster-demo/src/Broadcaster.cpp:296
#8 0x000055555569f309 in main () at ~/mediasoup-broadcaster-demo/src/main.cpp:103
这个请求没有响应。
此外,还会向服务端发送两个请求,分别在 mediasoup 服务器中为音频和视频创建 Producer:
#0 Broadcaster::OnProduce (this=0x7fffffffcc80, kind="", rtpParameters=...) at ~/mediasoup-broadcaster-demo/src/Broadcaster.cpp:147
#1 0x00005555576d9019 in mediasoupclient::SendTransport::Produce (this=0x3d4400031180, producerListener=0x7fffffffdbe0, track=0x3d440009d690,
encodings=0x7fffffffd200, codecOptions=0x0, codec=0x0, appData=...)
at ~/mediasoup-broadcaster-demo/build/_deps/mediasoupclient-src/src/Transport.cpp:229
#2 0x000055555565b0ad in Broadcaster::CreateSendTransport (this=0x7fffffffdbd0, enableAudio=true, useSimulcast=true)
at ~/mediasoup-broadcaster-demo/src/Broadcaster.cpp:438
#3 0x000055555565901e in Broadcaster::Start (this=0x7fffffffdbd0, baseUrl="https://192.168.217.129:4443/rooms/broadcaster", enableAudio=true, useSimulcast=true,
routerRtpCapabilities=..., verifySsl=false) at ~/mediasoup-broadcaster-demo/src/Broadcaster.cpp:296
#4 0x000055555569f309 in main () at ~/mediasoup-broadcaster-demo/src/main.cpp:103
请求格式如下:
json body =
{
{ "kind", kind },
{ "rtpParameters", rtpParameters }
};
/* clang-format on */
auto url = baseUrl + "/broadcasters/" + id + "/transports/" +
sendTransport->GetId() + "/producers";
std::cout << "Produce url: " << url << std::endl;
auto r = cpr::PostAsync(cpr::Url{url}, cpr::Body{body.dump()},
cpr::Header{{"Content-Type", "application/json"}},
cpr::VerifySsl{verifySsl})
.get();
响应格式如下:
{
"id":"8624d454-9519-436b-8da9-56755c1bd2b6"
}
返回一个 id。
消费媒体数据
一旦创建了 receive transport,客户端应用程序就可以使用它上的多个音频和视频 tracks。但是顺序是相反的 (这里消费者必须首先在服务器中创建)。
- 客户端应用程序向服务器发送它的 device.rtpCapabilities (它可能已经提前完成了)。
- 服务器应用程序应该检查远端设备是否可以使用特定的生产者 (也就是说,它是否支持生产者媒体编解码器)。它可以通过使用 router.canConsume() 方法来实现。
- 然后服务器应用程序在客户端为接收媒体数据而创建的 WebRTC transport 中调用 transport.consume() ,从而生成一个服务器端的 Consumer。
- 正如 transport.consume() 文档中所解释的,强烈建议使用 paused: true 创建服务器端 consumer,并在远程端点中创建 consumer 后恢复它。
- 服务器应用程序将 consumer 信息和参数传输到远程客户端应用程序,远程客户端应用程序在本地 receive transport 中调用 transport.consume()。
- 如果这是对 transport.consume() 的第一次调用,transport 将发出 “connect” 事件。
- 最后,在客户端将以一个 Consumer 实例解析 transport.consume()。
生产数据 (DataChannels)
一旦创建了 send transport,客户端应用程序就可以在其上生成多个 DataChannels。
- 应用程序在本地 send transport 中调用 transport.produceData()。
- 如果这是对 transport.produceData() 的第一次调用,则 transport 将发出 “connect” 事件。
- transport 将发出“producedata” 事件,因此应用程序将把事件参数传递给服务器,并在服务器端创建一个 DataProducer 实例。
- 最后,transport.produceData() 将在客户端使用 DataProducer 实例进行解析。
消费数据 (DataChannels)
一旦创建了 receive transport,客户端应用程序就可以使用它上的多个 DataChannels 了。但是顺序是相反的 (这里消费者必须首先在服务器中创建)。
- 服务器应用程序在客户端为接收数据而创建的 WebRTC transport 中调用 transport.consumeData(),从而生成一个服务器端的 DataConsumer。
- 服务器应用程序将 consumer 信息和参数传输到客户端应用程序,客户端应用程序在本地 receive transport 中调用 transport.consumeData()。
- 如果这是对 transport.consumeData() 的第一次调用,transport 将发出 “connect” 事件。
- 最后,在客户端将以一个 DataConsumer 实例解析 transport.consumeData()。
通信行为和事件
作为核心原则,调用 mediasoup 实例中的方法不会在该实例中生成直接事件。总之,这意味着在路 router、transport、producer、consumer、data producer 或 data consumer 上调用 close() 不会触发任何事件。
当一个 transport、producer、consumer、data producer 或 data consumer 在客户端或服务器端被关闭时 (例如通过在它上调用 close()),应用程序应该向另一端发出它的关闭信号,另一端也应该在相应的实体上调用 close()。另外,服务器端应用程序应该监听以下关闭事件并通知客户端:
- Transport “routerclose”。客户端应该在对应的本地 transport 中调用
close()
。 - Producer “transportclose”。客户端应该在对应的本地 producer 中调用
close()
。 - Consumer “transportclose”。客户端应该在对应的本地 consumer 中调用
close()
。 - Consumer “producerclose”。客户端应该在对应的本地 consumer 中调用
close()
。 - DataProducer “transportclose”。客户端应该在对应的本地 data producer 中调用
close()
。 - DataConsumer “transportclose”。客户端应该在对应的本地 data consumer 中调用
close()
。 - DataConsumer “dataproducerclose”。客户端应该在对应的本地 data consumer 中调用
close()
。
在客户端或服务器端暂停 RTP 生产者或消费者时也会发生同样的情况。行为必须向对方发出信号。另外,服务器端应用程序应该监听以下事件并通知客户端:
- Consumer “producerpause”。客户端应该在对应的本地 transport 中调用
pause()
。 - Consumer “producerresume”。客户端应该在对应的本地 transport 中调用
resume()
(除非 consumer 本身也被故意暂停)。
当使用 simulcast 或 SVC 时,应用程序可能会对客户端和服务器端消费者之间的首选层和有效层感兴趣。
- 服务器端应用程序通过 consumer.setPreferredLayers() 设置 consumer 首选层。
- 服务器端 consumer 订阅 “layerschange” 事件,并通知客户端应用程序正在传输的有效层。
参考文档:
Communication Between Client and Server
Mediao Soup Demo协议分析
mediasoup protocol
网友评论