mediasoup-client 和 libmediasoupclient 指南

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 protocolMEDIASOUP_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);
        });

客户端库中,设备加载实现的大体流程如下:

  1. 获取本地的 RTP capabilities。
  2. 根据本地的 RTP capabilities 和传进来的服务器端 router 的 RTP capabilities 生成扩展的 RTP capabilities。
  3. 根据扩展的 RTP capabilities 生成接收的 RTP capabilities 和 SCTP capabilities。

在这个上下文中,capabilities 指的主要是端点支持的音视频编解码器配置、RTP 扩展和 RTCP 反馈等。

创建 Transports

mediasoup-client 和 libmediasoupclient 都需要将 WebRTC 传输的发送和接收分开。通常客户端应用程序会提前创建这些 transports,甚至在想要发送或接收媒体数据之前。即对于发送和接收,分别创建 WebRTC 的 PeerConnectionInterface 连接。

对于发送媒体数据:

mediasoup-broadcaster-demomediasoup-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 的完整过程如下:

  1. 如上所述,发送请求,让服务端为 broadcaster 创建 send transport,获得为这个 transport 分配的 ID 以及连接参数,包括 ICE 参数,ICE candidates,SCTP 参数和 DTLS 参数。
  2. 创建 SendTransport 对象,这个过程中还会一连串创建多个对象: SendTransport -> SendHandler -> Handler -> PeerConnection -> webrtc::PeerConnectionInterface
  3. 如果要发送音频,会创建 audio 的 Track,并通过这样一个调用过程将 audio 的 track 添加给 webrtc::PeerConnectionInterfaceSendTransport::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。
  4. 如果要发送视频,会创建 video 的 Track,通过与音频类似的过程,将 video 的 track 添加给 webrtc::PeerConnectionInterface,并基于 video 的 track 创建 Producer。如果既要发送音频,也要发送视频,则会对 peer connection 分别为音频和视频各执行一次(创建 Offer,设置 local description,创建 Answer,设置 remote description)的过程。
  5. 为 transport 创建 DataProducer。

对于接收媒体数据:

为接收媒体向信令服务发送的请求及请求的响应与上面发送媒体数据时的完全相同,这里不再赘述。客户端创建 SendTransport 的完整过程如下:

  1. 如上所述,发送请求,让服务端为 broadcaster 创建 transport,获得为这个 transport 分配的 ID 以及连接参数,包括 ICE 参数,ICE candidates,SCTP 参数和 DTLS 参数。
  2. 创建 RecvTransport 对象,这个过程中还会一连串创建多个对象: RecvTransport -> RecvHandler -> Handler -> PeerConnection -> webrtc::PeerConnectionInterface
  3. 创建 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 时,应用程序可能会对客户端和服务器端消费者之间的首选层和有效层感兴趣。

参考文档:
Communication Between Client and Server
Mediao Soup Demo协议分析
mediasoup protocol

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容