mediasoup-client 和 libmediasoupclient 指南
mediasoup 是一個 SFU,先來看一下 mediasoup 的架構,看一下 mediasoup 的主要概念和抽象,如 Transport,Producer,Consumer,DataProducer,DataConsumer 在 mediasoup 的媒體數據轉發系統中的位置和作用:
一般來說,這些抽象在 mediasoup SFU 中有實體對象與之對應。這些抽象在 libmediasoupclient 中對應 C++ 類的含義可以參考 API 文檔。這些抽象的名稱是以 mediasoup SFU 服務器端為中心建立的,如 Producer,指的是可以給 mediasoup SFU 服務器提供媒體數據的實體,是 mediasoup SFU 服務器的媒體數據生產者,對應到 libmediasoupclient 中的 C++ 類 Producer,它是音頻源/音頻采集模塊或視頻源/視頻采集模塊的代理。
對于 mediasoup 來說,信令協議所要完成的功能就是在適當的時候協調 mediasoup SFU 服務器完成媒體轉發所需要的這些服務端對象的創建,資源的分配,和鏈路的打通,并協調客戶端與服務端的連接建立,及數據的發送、接收和控制,協調客戶端和服務器之間交換 mediasoup 相關的參數、請求/響應和通知。
mediasoup 本身不提供任何信令協議來幫助客戶端和服務器進行通信。信令的傳遞取決于應用程序,它們可以使用 WebSocket、HTTP 或其它通信方式進行通信,并在客戶端和服務器之間交換 mediasoup 相關的參數、請求/響應和通知。在大多數情況下,服務端可能需要主動向客戶端遞送消息或事件通知等,則這種通信必須是雙向的,因此通常需要全雙工通道。但是,應用程序可以重用相同的通道來進行非 mediasoup 相關的消息交換 ( 例如身份驗證過程、聊天消息、文件傳輸和任何應用程序希望實現的內容)。
更準確地說,在 mediasoup v2 的時候,還有信令協議,具體內容如 mediasoup protocol 和 MEDIASOUP_PROTOCOL.md 的說明。但在最新的 v3 版中,已經沒有這部分了。信令協議需要服務端應用自己實現。
信令協議協議實現的示例可以參考 mediasoup-demo 的 server 的 mediasoup-demo/server/server.js。
讓我們假設我們的 JavaScript 或 C++ 客戶端應用程序初始化了一個 mediasoup-client?Device 或一個 libmediasoupclient?Device 對象,連接一個 mediasoup?Router (已經在服務器中創建)并基于 WebRTC 發送和接收媒體數據。
這里 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);});}); }mediasoup-client (客戶端 JavaScript 庫) 和 libmediasoupclient (基于 libwebrtc 的 C++ 庫) 都生成適用于 mediasoup 的 RTP 參數,這簡化了客戶端應用程序的開發。
信令和 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 字符串。
創建 Transports
mediasoup-client 和 libmediasoupclient 都需要將 WebRTC 傳輸的發送和接收分開。通常客戶端應用程序會提前創建這些 transports,甚至在想要發送或接收媒體數據之前。
對于發送媒體數據:
- WebRTC transport 必須首先在 mediasoup router 中創建:?router.createWebRtcTransport()。
- 然后重復地在客戶端應用程序中創建:device.createSendTransport()。
- 客戶端應用程序必須訂閱本地 transport 中的 “connect” 和 “produce” 事件。
對于 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 的信息。
在 mediasoup router 中創建 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} }對于接收媒體數據:
- WebRTC transport 必須首先在 mediasoup router 中創建:?router.createWebRtcTransport()。
- 然后重復地在客戶端應用程序中創建:device.createRecvTransport()。
- 客戶端應用程序必須訂閱本地 transport 中的 “connect” 和 “produce” 事件。
如果在這些 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 實例進行解析。
這里的把事件參數傳遞給服務器,對應于 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
總結
以上是生活随笔為你收集整理的mediasoup-client 和 libmediasoupclient 指南的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: WebRTC 视频发送和接收处理过程
- 下一篇: WebRTC 中收集音视频编解码能力