mediasoup-demo 運(yùn)行實(shí)戰(zhàn)

mediasoup 是一個(gè)強(qiáng)大的 WebRTC SFU 服務(wù)。mediasoup-demo 則是 mediasoup 的一個(gè)很不錯(cuò)的入門演示程序。這里記錄把 mediasoup-demo 跑起來的過程。操作系統(tǒng)平臺(tái)以 Ubuntu 20.04 為例。mediasoup 主要以 JavaScript 開發(fā),運(yùn)行環(huán)境為 Node.js,它一般作為 Node.js 模塊運(yùn)行于 Node.js 應(yīng)用中。

準(zhǔn)備環(huán)境

mediasoup v3 的 安裝指南 中有安裝要求:

  • node version >= v12.0.0
  • python version >= 3.6 with PIP
  • GNU make

對(duì)于 Linux,OSX 和任何 *NIX 系統(tǒng),還有額外的要求:

  • gcc 和 g++ >= 4.9 或 clang (包含 C++11 支持)
  • cc 和 c++ 命令 (符號(hào)連接) 指向?qū)?yīng)的 gcc/g++ 或 clang/clang++ 可執(zhí)行文件。

當(dāng)系統(tǒng)軟件庫中的 node 版本不符合要求時(shí),需要自己安裝適當(dāng)版本的 Node。Node 版本不合適,很有可能 demo 就運(yùn)行不起來。筆者試了多個(gè)版本都沒能把 mediasoup 跑起來,包括 v13.1.0、v13.10.0 和最新的長(zhǎng)期支持版 v16.13.1,不過在 sequelize 的 GitHub issue 12419 ,看到有人提到用 v12.18.3 解決了筆者遇到的一些問題,筆者也選擇了 v12.18.3 版。

下載 node 的預(yù)編譯壓縮包:

https://nodejs.org/dist/

如果系統(tǒng)中已經(jīng)安裝了其它版本的 node,在安裝之前,還需要先移除之前安裝的版本:

sudo rm -rf /usr/local/bin/npm /usr/local/share/man/man1/node* ~/.npm
sudo rm -rf /usr/local/lib/node*
sudo rm -rf /usr/local/bin/node*
sudo rm -rf /usr/local/include/node*

sudo apt-get purge nodejs npm
sudo apt autoremove

解壓并安裝 node:

tar -xf node-v12.18.3-linux-x64.tar.xz
sudo mv node-v12.18.3-linux-x64/bin/* /usr/local/bin/
sudo mv node-v12.18.3-linux-x64/lib/node_modules/ /usr/local/lib/

安裝 mediasoup-demo

下載 mediasoup-demo,克隆 mediasoup-demo 工程

$ git clone https://github.com/versatica/mediasoup-demo.git
$ cd mediasoup-demo
$ git checkout v3

設(shè)置 mediasoup-demo server:

mediasoup-demo$ cd server
server$ npm install

拷貝 config.example.jsconfig.js,并對(duì)它做一些定制化的修改:

$ cp config.example.js config.js

這一步是必須的,否則 mediasoup-demo 運(yùn)行將出錯(cuò)。所需要做的配置包括域名,監(jiān)聽的 IP 地址,HTTPS 證書和私鑰的路徑

域名、監(jiān)聽 HTTPS 的 IP/端口、證書路徑及私鑰路徑:

    domain : process.env.DOMAIN || 'localhost',
    // Signaling settings (protoo WebSocket server and HTTP API server).
    https  :
    {
        listenIp   : '0.0.0.0',
        // NOTE: Don't change listenPort (client app assumes 4443).
        listenPort : process.env.PROTOO_LISTEN_PORT || 4443,
        // NOTE: Set your own valid certificate files.
        tls        :
        {
            cert : process.env.HTTPS_CERT_FULLCHAIN || `${__dirname}/certs/fullchain.pem`,
            key  : process.env.HTTPS_CERT_PRIVKEY || `${__dirname}/certs/privkey.pem`
        }
    },

這些配置可以通過修改 config.js 實(shí)現(xiàn),也可以通過設(shè)置環(huán)境變量實(shí)現(xiàn)。這里不修改這些配置,將 TLS 證書和私鑰放進(jìn) mediasoup-demo/server/certs/ 并按照這里的配置重命名。如果已經(jīng)有網(wǎng)站域名,網(wǎng)站已經(jīng)開了 HTTPS,且打算將 mediasoup-demo 跑在網(wǎng)站同一臺(tái)機(jī)器上,可以將證書和私鑰拷貝過來,或者用環(huán)境變量 HTTPS_CERT_FULLCHAINHTTPS_CERT_PRIVKEY 分別指向證書和私鑰的路徑:

export HTTPS_CERT_FULLCHAIN="XXX"
export HTTPS_CERT_PRIVKEY="YYY"

否則,可以用工具 https://github.com/aggresss/playground-cpp/blob/master/certs/autogen.sh 生成臨時(shí)的自簽名證書,運(yùn)行這個(gè)腳本生成如下文件:

playground-cpp/certs$ git status
位于分支 master
您的分支與上游分支 'origin/master' 一致。

尚未暫存以備提交的變更:
  (使用 "git add <文件>..." 更新要提交的內(nèi)容)
  (使用 "git restore <文件>..." 丟棄工作區(qū)的改動(dòng))
    修改:     ca.crt
    修改:     ca.csr
    修改:     ca.key
    修改:     ca.srl
    修改:     client.crt
    修改:     client.csr
    修改:     client.key
    修改:     md5.txt
    修改:     server.crt
    修改:     server.csr
    修改:     server.key

server.key 是私鑰,server.crt 是證書。將這兩個(gè)文件拷貝到 mediasoup-demo 下:

playground-cpp/certs$ mkdir -p ~/mediasoup-demo/server/certs/ 
playground-cpp/certs$ mv server.key ~/mediasoup-demo/server/certs/privkey.pem 
playground-cpp/certs$ mv server.crt ~/mediasoup-demo/server/certs/fullchain.pem 

沒有私鑰和證書,運(yùn)行服務(wù)器應(yīng)用時(shí)會(huì)報(bào)錯(cuò)找不到證書:

  mediasoup-demo-server:INFO running an HTTPS server... +6ms
(node:396580) UnhandledPromiseRejectionWarning: Error: ENOENT: no such file or directory, open '~/mediasoup-demo/server/certs/fullchain.pem'
    at Object.openSync (fs.js:462:3)
    at Object.readFileSync (fs.js:364:35)
    at runHttpsServer (~/mediasoup-demo/server/server.js:431:13)
    at run (~/mediasoup-demo/server/server.js:74:8)
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
(node:396580) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:396580) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

運(yùn)行瀏覽器應(yīng)用時(shí)會(huì)報(bào)錯(cuò)找不到私鑰:

[16:02:19] Finished '<anonymous>' after 27 ms
[16:02:19] Finished 'live' after 14 s
internal/fs/utils.js:269
    throw err;
    ^

Error: ENOENT: no such file or directory, open '~/mediasoup-demo/server/certs/privkey.pem'
    at Object.openSync (fs.js:462:3)
    at Object.readFileSync (fs.js:364:35)
    at getKey (~/mediasoup-demo/app/node_modules/browser-sync/dist/server/utils.js:38:15)
    at getHttpsServerDefaults (~/mediasoup-demo/app/node_modules/browser-sync/dist/server/utils.js:45:14)
    at Object.getHttpsOptions (~/mediasoup-demo/app/node_modules/browser-sync/dist/server/utils.js:67:41)
    at ~/mediasoup-demo/app/node_modules/browser-sync/dist/server/utils.js:81:44
    at Object.getServer (~/mediasoup-demo/app/node_modules/browser-sync/dist/server/utils.js:85:15)
    at createServer (~/mediasoup-demo/app/node_modules/browser-sync/dist/server/static-server.js:71:24)
    at createServer (~/mediasoup-demo/app/node_modules/browser-sync/dist/server/index.js:72:42)
    at module.exports.plugin (~/mediasoup-demo/app/node_modules/browser-sync/dist/server/index.js:12:20) {
  errno: -2,
  syscall: 'open',
  code: 'ENOENT',
  path: '~/mediasoup-demo/server/certs/privkey.pem'
}
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! mediasoup-demo-app@3.0.0 start: `gulp live`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the mediasoup-demo-app@3.0.0 start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     ~/.npm/_logs/2021-12-24T08_02_19_135Z-debug.log

還有一個(gè)必須要配置的是 RTC 傳輸選項(xiàng)中的監(jiān)聽 IP MEDIASOUP_LISTEN_IP

        webRtcTransportOptions :
        {
            listenIps :
            [
                {
                    ip          : process.env.MEDIASOUP_LISTEN_IP || '1.2.3.4',
                    announcedIp : process.env.MEDIASOUP_ANNOUNCED_IP
                }
            ],
            initialAvailableOutgoingBitrate : 1000000,
            minimumAvailableOutgoingBitrate : 600000,
            maxSctpMessageSize              : 262144,
            // Additional options that are not part of WebRtcTransportOptions.
            maxIncomingBitrate              : 1500000
        },

這個(gè)選項(xiàng)同樣既可以通過修改 config.js 配置,也可以通過設(shè)置環(huán)境變量配置。監(jiān)聽的 IP 地址需要設(shè)置為機(jī)器本地 IP 地址。否則,瀏覽器應(yīng)用運(yùn)行和服務(wù)器通信時(shí),服務(wù)器將報(bào)錯(cuò):

  mediasoup:Router createWebRtcTransport() +3s
  mediasoup:Channel request() [method:router.createWebRtcTransport, id:5] +3s
  mediasoup:ERROR:Channel [pid:396733 RTC::PortManager::Bind() | throwing MediaSoupError: port bind failed due to address not available [transport:udp, ip:'1.2.3.4', port:42251, attempt:1/10000] +0ms
  mediasoup:ERROR:Channel [pid:396733 Worker::OnChannelRequest() | throwing MediaSoupError: port bind failed due to address not available [transport:udp, ip:'1.2.3.4', port:42251, attempt:1/10000] [method:router.createWebRtcTransport] +4ms
  mediasoup:WARN:Channel request failed [method:router.createWebRtcTransport, id:5]:  [method:router.createWebRtcTransport] +0ms
  mediasoup-demo-server:ERROR:Room request failed:Error:  [method:router.createWebRtcTransport] at Channel.processMessage (~/opensource/mediasoup-demo/server/node_modules/mediasoup/node/lib/Channel.js:195:37) at Socket.<anonymous> (~/opensource/mediasoup-demo/server/node_modules/mediasoup/node/lib/Channel.js:69:34)     at Socket.emit (events.js:315:20)     at Socket.EventEmitter.emit (domain.js:483:12)     at addChunk (_stream_readable.js:295:12)     at readableAddChunk (_stream_readable.js:271:9)     at Socket.Readable.push (_stream_readable.js:212:10)     at Pipe.onStreamRead (internal/stream_base_commons.js:186:23) +0ms
  mediasoup-demo-server:Room protoo Peer "close" event [peerId:ucayshrc] +27ms
  mediasoup-demo-server:INFO:Room last Peer in the room left, closing the room [roomId:rvpgogc7] +3s
  mediasoup-demo-server:Room close() +2ms
  mediasoup:Router close() +28ms
  mediasoup:Channel request() [method:router.close, id:6] +27ms
  mediasoup:Transport routerClosed() +3s
  mediasoup:DataProducer transportClosed() +3s
  mediasoup:RtpObserver routerClosed() +3s
  mediasoup:Channel request succeeded [method:router.close, id:6] +4ms

報(bào)錯(cuò)提示,由于 IP 地址不可用,綁定端口失敗。

設(shè)置 mediasoup-demo 瀏覽器應(yīng)用:

$ cd app
$ npm install

本地運(yùn)行 mediasoup-demo

在終端中運(yùn)行 Node.js 服務(wù)器應(yīng)用:

mediasoup-demo$ cd server
mediasoup-demo/server$ npm start

在一個(gè)不同的終端中編譯并運(yùn)行瀏覽器應(yīng)用程序:

mediasoup-demo$ cd app
mediasoup-demo/app$ npm start

然后就可以通過瀏覽器訪問 mediasoup 了,如:

https://192.168.217.129:3000/?info=true&roomId=rvpgogc7
1640340445057.jpg

mediasoup-demo 這個(gè)運(yùn)行起來之后,網(wǎng)絡(luò)拓?fù)涫沁@樣的:

無標(biāo)題演示文稿.png

mediasoup-demo/server 是 WebRTC 的 SFU 服務(wù)器,mediasoup-demo/app 是客戶端瀏覽器應(yīng)用服務(wù)器,用于提供網(wǎng)頁和 JS 文件等資源。

mediasoup-broadcaster-demo

mediasoup 項(xiàng)目還提供了一個(gè) libmediasoupclient 的演示程序 mediasoup-broadcaster-demo,它運(yùn)行起來后可以與上面跑起來的 mediasoup-demo 系統(tǒng)中的示例 Web 應(yīng)用互通,可以發(fā)送一些構(gòu)造的音視頻流給示例 Web 應(yīng)用。

這里看下在 Ubuntu 20.04 Linux 平臺(tái)編譯并運(yùn)行 mediasoup-broadcaster-demo 的過程。

下載并配置編譯 mediasoup-broadcaster-demo

opensource$ git clone https://github.com/versatica/mediasoup-broadcaster-demo.git
opensource$ cd mediasoup-broadcaster-demo
mediasoup-broadcaster-demo$ cmake . -Bbuild                                              \
  -DLIBWEBRTC_INCLUDE_PATH:PATH=~/data/opensource/webrtc-checkout/src \
  -DLIBWEBRTC_BINARY_PATH:PATH=~/data/opensource/webrtc-checkout/src/out/m96/obj   \
  -DOPENSSL_INCLUDE_DIR:PATH=/usr/include/       \
  -DCMAKE_USE_OPENSSL=ON    \
  -DCMAKE_BUILD_TYPE=Debug

注意正確配置 LIBWEBRTC_INCLUDE_PATHLIBWEBRTC_BINARY_PATH 分別為下載的 webrtc 的源代碼路徑和編譯出來的目標(biāo)文件路徑。筆者這里在編譯時(shí),用了系統(tǒng)安裝的 openssl 相關(guān)庫,因而用于配置 openssl 頭文件路徑的 OPENSSL_INCLUDE_DIR 指向了系統(tǒng) /usr/include/。另外,為了方便后面的動(dòng)態(tài)調(diào)試,這里加了 -DCMAKE_BUILD_TYPE=Debug

完成了上面的配置之后,執(zhí)行如下命令編譯源碼:

mediasoup-broadcaster-demo$ make -C build/

由于 mediasoup-broadcaster-demo 中的一些代碼,和筆者本地的 webrtc 的版本不匹配,出現(xiàn)了如下的編譯錯(cuò)誤:

[  4%] Building CXX object libwebrtc/CMakeFiles/webrtc_broadcaster.dir/test/testsupport/ivf_video_frame_generator.cc.o
~/MyProjects/mediasoup-broadcaster-demo/deps/libwebrtc/test/testsupport/ivf_video_frame_generator.cc: In constructor ‘webrtc::test::IvfVideoFrameGenerator::IvfVideoFrameGenerator(const string&)’:
~/MyProjects/mediasoup-broadcaster-demo/deps/libwebrtc/test/testsupport/ivf_video_frame_generator.cc:48:18: error: ‘class webrtc::VideoCodec’ has no member named ‘buffer_pool_size’
   48 |   codec_settings.buffer_pool_size = std::numeric_limits<int>::max();
      |                  ^~~~~~~~~~~~~~~~
In file included from ~/opensource/webrtc-checkout/src/api/sequence_checker.h:13,
                 from ~/MyProjects/mediasoup-broadcaster-demo/deps/libwebrtc/test/testsupport/ivf_video_frame_generator.h:18,
                 from ~/MyProjects/mediasoup-broadcaster-demo/deps/libwebrtc/test/testsupport/ivf_video_frame_generator.cc:11:
~/MyProjects/mediasoup-broadcaster-demo/deps/libwebrtc/test/testsupport/ivf_video_frame_generator.cc:52:23: error: ‘class webrtc::VideoDecoder’ has no member named ‘InitDecode’; did you mean ‘Decode’?
   52 |       video_decoder_->InitDecode(&codec_settings, /*number_of_cores=*/1),
      |                       ^~~~~~~~~~
~/opensource/webrtc-checkout/src/rtc_base/checks.h:393:22: note: in definition of macro ‘RTC_CHECK_OP’
  393 |   ::rtc::Safe##name((val1), (val2))                        \
      |                      ^~~~
~/MyProjects/mediasoup-broadcaster-demo/deps/libwebrtc/test/testsupport/ivf_video_frame_generator.cc:51:3: note: in expansion of macro ‘RTC_CHECK_EQ’
   51 |   RTC_CHECK_EQ(
      |   ^~~~~~~~~~~~
~/MyProjects/mediasoup-broadcaster-demo/deps/libwebrtc/test/testsupport/ivf_video_frame_generator.cc:52:23: error: ‘class webrtc::VideoDecoder’ has no member named ‘InitDecode’; did you mean ‘Decode’?
   52 |       video_decoder_->InitDecode(&codec_settings, /*number_of_cores=*/1),
      |                       ^~~~~~~~~~
~/opensource/webrtc-checkout/src/rtc_base/checks.h:397:60: note: in definition of macro ‘RTC_CHECK_OP’
  397 |             ::rtc::webrtc_checks_impl::LogStreamer<>() << (val1) << (val2)
      |                                                            ^~~~
~/MyProjects/mediasoup-broadcaster-demo/deps/libwebrtc/test/testsupport/ivf_video_frame_generator.cc:51:3: note: in expansion of macro ‘RTC_CHECK_EQ’
   51 |   RTC_CHECK_EQ(
      |   ^~~~~~~~~~~~
make[2]: *** [libwebrtc/CMakeFiles/webrtc_broadcaster.dir/build.make:167:libwebrtc/CMakeFiles/webrtc_broadcaster.dir/test/testsupport/ivf_video_frame_generator.cc.o] 錯(cuò)誤 1
make[2]: 離開目錄“~/MyProjects/mediasoup-broadcaster-demo/build”

上面這個(gè)錯(cuò)誤來源于 mediasoup-broadcaster-demo/deps/libwebrtc/test/testsupport/ivf_video_frame_generator.cc 文件,這里粗暴地注釋掉相關(guān)的代碼:

mediasoup-broadcaster-demo$ git diff 
diff --git a/deps/libwebrtc/test/testsupport/ivf_video_frame_generator.cc b/deps/libwebrtc/test/testsupport/ivf_video_frame_generator.cc
index e5c4c5f..788467a 100644
--- a/deps/libwebrtc/test/testsupport/ivf_video_frame_generator.cc
+++ b/deps/libwebrtc/test/testsupport/ivf_video_frame_generator.cc
@@ -45,12 +45,12 @@ IvfVideoFrameGenerator::IvfVideoFrameGenerator(const std::string& file_name)
   // Set buffer pool size to max value to ensure that if users of generator,
   // ex. test frameworks, will retain frames for quite a long time, decoder
   // won't crash with buffers pool overflow error.
-  codec_settings.buffer_pool_size = std::numeric_limits<int>::max();
-  RTC_CHECK_EQ(video_decoder_->RegisterDecodeCompleteCallback(&callback_),
-               WEBRTC_VIDEO_CODEC_OK);
-  RTC_CHECK_EQ(
-      video_decoder_->InitDecode(&codec_settings, /*number_of_cores=*/1),
-      WEBRTC_VIDEO_CODEC_OK);
+//  codec_settings.buffer_pool_size = std::numeric_limits<int>::max();
+//  RTC_CHECK_EQ(video_decoder_->RegisterDecodeCompleteCallback(&callback_),
+//               WEBRTC_VIDEO_CODEC_OK);
+//  RTC_CHECK_EQ(
+//      video_decoder_->InitDecode(&codec_settings, /*number_of_cores=*/1),
+//      WEBRTC_VIDEO_CODEC_OK);
 }
 IvfVideoFrameGenerator::~IvfVideoFrameGenerator() {
   MutexLock lock(&lock_);

完成了上面的代碼修改,重新編譯之后,出現(xiàn)了如下的鏈接錯(cuò)誤:

[ 86%] Linking CXX executable broadcaster
/usr/bin/ld: ~/opensource/webrtc-checkout/src/out/m96/obj/libwebrtc.a(audio_device_alsa_linux.o): in function `webrtc::AudioDeviceLinuxALSA::Init()':
~/opensource/webrtc-checkout/src/out/m96/../../modules/audio_device/linux/audio_device_alsa_linux.cc:158: undefined reference to `XOpenDisplay'
/usr/bin/ld: ~/opensource/webrtc-checkout/src/out/m96/obj/libwebrtc.a(audio_device_alsa_linux.o): in function `webrtc::AudioDeviceLinuxALSA::Terminate()':
~/opensource/webrtc-checkout/src/out/m96/../../modules/audio_device/linux/audio_device_alsa_linux.cc:189: undefined reference to `XCloseDisplay'
/usr/bin/ld: ~/opensource/webrtc-checkout/src/out/m96/obj/libwebrtc.a(audio_device_alsa_linux.o): in function `webrtc::AudioDeviceLinuxALSA::KeyPressed() const':
~/opensource/webrtc-checkout/src/out/m96/../../modules/audio_device/linux/audio_device_alsa_linux.cc:1624: undefined reference to `XQueryKeymap'
 . . . . . .
/usr/bin/ld: ~/opensource/webrtc-checkout/src/out/m96/../../base/message_loop/message_pump_glib.cc:181: undefined reference to `g_main_context_default'
/usr/bin/ld: ~/opensource/webrtc-checkout/src/out/m96/../../base/message_loop/message_pump_glib.cc:183: undefined reference to `g_main_context_new'
/usr/bin/ld: ~/opensource/webrtc-checkout/src/out/m96/../../base/message_loop/message_pump_glib.cc:184: undefined reference to `g_main_context_push_thread_default'
/usr/bin/ld: ~/opensource/webrtc-checkout/src/out/m96/../../base/message_loop/message_pump_glib.cc:199: undefined reference to `g_source_new'
/usr/bin/ld: ~/opensource/webrtc-checkout/src/out/m96/../../base/message_loop/message_pump_glib.cc:201: undefined reference to `g_source_add_poll'
/usr/bin/ld: ~/opensource/webrtc-checkout/src/out/m96/../../base/message_loop/message_pump_glib.cc:202: undefined reference to `g_source_set_priority'
/usr/bin/ld: ~/opensource/webrtc-checkout/src/out/m96/../../base/message_loop/message_pump_glib.cc:204: undefined reference to `g_source_set_can_recurse'
/usr/bin/ld: ~/opensource/webrtc-checkout/src/out/m96/../../base/message_loop/message_pump_glib.cc:205: undefined reference to `g_source_attach'

這個(gè)鏈接錯(cuò)誤是因?yàn)?Linux 版 webrtc 代碼依賴了 X11 和 glib-2.0 庫,但 mediasoup-broadcaster-demoCMakeLists.txt 中并沒有配置對(duì)這兩個(gè)庫的依賴。這里簡(jiǎn)單地添加對(duì)這兩個(gè)庫的依賴:

mediasoup-broadcaster-demo$ git diff 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b8c40a4..d06c499 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -90,5 +90,7 @@ target_link_libraries(${PROJECT_NAME} PUBLIC
        cpr
        mediasoupclient
        webrtc_broadcaster
+       X11
+       glib-2.0
 )

隨后重新編譯,則順利完成。

mediasoup-broadcaster-demo$ make -C build/

運(yùn)行 mediasoup-broadcaster-demo

mediasoup-broadcaster-demo 編譯生成可執(zhí)行文件 build/broadcaster。這個(gè)可執(zhí)行文件的一些運(yùn)行配置需要通過環(huán)境變量來完成。SERVER_URL 環(huán)境變量需要指向服務(wù)器的地址,也就是上面 mediasoup-demo 中 server 那個(gè)服務(wù)的地址,ROOM_ID 則指向房間號(hào)。此外,需要先在瀏覽器中,把上面 mediasoup-demo 中的示例 Web 應(yīng)用跑起來,并加入相同的房間,如用瀏覽器打開 https://192.168.217.129:3000/?roomId=broadcaster,否則這里的 build/broadcaster 會(huì)由于找不到房間而直接失敗退出。

運(yùn)行 build/broadcaster

mediasoup-broadcaster-demo$ export SERVER_URL=https://192.168.217.129:4443
mediasoup-broadcaster-demo$ export ROOM_ID=broadcaster
mediasoup-broadcaster-demo$ build/broadcaster
[DEBUG] mediasoupclient::Initialize() | mediasoupclient v3.3.0
(field_trial.cc:140): Setting field trial string:WebRTC-SupportVP9SVC/EnabledByFlag_3SL3TL/
[INFO] welcome to mediasoup broadcaster app!

[INFO] verifying that room 'broadcaster' exists...
[ERROR] unable to retrieve room info [status code:0, body:""]

筆者是在本地環(huán)境中部署的 mediasoup-demo,沒有域名,也沒有正式的 PKI 證書,而只是有一個(gè)本地生成的自簽名證書。所以上面 build/broadcaster 跑起來后就立即由于 SSL 證書驗(yàn)證失敗而結(jié)束了。在代碼中關(guān)掉 build/broadcaster 的 SSL 驗(yàn)證:

diff --git a/src/main.cpp b/src/main.cpp
index 21f8bcc..f84cb6f 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -57,7 +57,7 @@ int main(int /*argc*/, char* /*argv*/[])
        if (envUseSimulcast && std::string(envUseSimulcast) == "false")
                useSimulcast = false;
 
-       bool verifySsl = true;
+       bool verifySsl = false;
        if (envVerifySsl && std::string(envVerifySsl) == "false")
                verifySsl = false;

隨后再次運(yùn)行,則能在 mediasoup-demo 中的示例 Web 應(yīng)用的瀏覽器窗口中看到 build/broadcaster 發(fā)過來的畫面:

1640831833321.jpg

窗口中間五顏六色的框框即是 build/broadcaster 發(fā)過來的畫面

mediasoup-broadcaster-demo 的音頻源和視頻源

在 WebRTC 的概念體系中,Track 用于將音頻源或者視頻源接入整個(gè)音視頻數(shù)據(jù)處理編碼發(fā)送流水線中。mediasoup-broadcaster-demo/src/Broadcaster.cpp 文件的 Broadcaster::CreateSendTransport(bool enableAudio, bool useSimulcast) 函數(shù)里可以看到如下這段代碼:

  ///////////////////////// Create Audio Producer //////////////////////////

  if (enableAudio && this->device.CanProduce("audio")) {
    auto audioTrack = createAudioTrack(std::to_string(rtc::CreateRandomId()));

    /* clang-format off */
        json codecOptions = {
            { "opusStereo", true },
            { "opusDtx",        true }
        };
    /* clang-format on */

    this->sendTransport->Produce(this, audioTrack, nullptr, &codecOptions,
                                 nullptr);
  } else {
    std::cerr << "[WARN] cannot produce audio" << std::endl;
  }

  ///////////////////////// Create Video Producer //////////////////////////

  if (this->device.CanProduce("video")) {
    auto videoTrack =
        createSquaresVideoTrack(std::to_string(rtc::CreateRandomId()));

    if (useSimulcast) {
      std::vector<webrtc::RtpEncodingParameters> encodings;
      encodings.emplace_back(webrtc::RtpEncodingParameters());
      encodings.emplace_back(webrtc::RtpEncodingParameters());
      encodings.emplace_back(webrtc::RtpEncodingParameters());

      this->sendTransport->Produce(this, videoTrack, &encodings, nullptr,
                                   nullptr);
    } else {
      this->sendTransport->Produce(this, videoTrack, nullptr, nullptr, nullptr);
    }
  } else {
    std::cerr << "[WARN] cannot produce video" << std::endl;

    return;
  }

這里分別調(diào)用 createAudioTrack()createSquaresVideoTrack() 創(chuàng)建音頻的 track 和視頻的 track。在 mediasoup-broadcaster-demo/src/MediaStreamTrackFactory.cpp 文件中,這兩個(gè)函數(shù)的實(shí)現(xiàn)如下:

// Audio track creation.
rtc::scoped_refptr<webrtc::AudioTrackInterface> createAudioTrack(
    const std::string& label) {
  if (!factory)
    createFactory();

  cricket::AudioOptions options;
  options.highpass_filter = false;

  rtc::scoped_refptr<webrtc::AudioSourceInterface> source =
      factory->CreateAudioSource(options);

  return factory->CreateAudioTrack(label, source);
}

// Video track creation.
rtc::scoped_refptr<webrtc::VideoTrackInterface> createVideoTrack(
    const std::string& /*label*/) {
  if (!factory)
    createFactory();

  auto* videoTrackSource =
      new rtc::RefCountedObject<webrtc::FakePeriodicVideoTrackSource>(
          false /* remote */);

  return factory->CreateVideoTrack(rtc::CreateRandomUuid(), videoTrackSource);
}

rtc::scoped_refptr<webrtc::VideoTrackInterface> createSquaresVideoTrack(
    const std::string& /*label*/) {
  if (!factory)
    createFactory();

  std::cout << "[INFO] getting frame generator" << std::endl;
  auto* videoTrackSource =
      new rtc::RefCountedObject<webrtc::FrameGeneratorCapturerVideoTrackSource>(
          webrtc::FrameGeneratorCapturerVideoTrackSource::Config(),
          webrtc::Clock::GetRealTimeClock(), false);
  videoTrackSource->Start();

  std::cout << "[INFO] creating video track" << std::endl;
  return factory->CreateVideoTrack(rtc::CreateRandomUuid(), videoTrackSource);
}

對(duì)于 createAudioTrack(),它通過 webrtc::PeerConnectionFactoryInterface 創(chuàng)建了表示麥克風(fēng)的音頻源。對(duì)于 createSquaresVideoTrack() ,它則創(chuàng)建了類型為 webrtc::FrameGeneratorCapturerVideoTrackSource 的視頻源,通過 WebRTC 中這個(gè)組件的代碼,我們可以看到,它不操作攝像頭,而是在內(nèi)存中構(gòu)造了一些花花綠綠的方塊出來。這也就是我們?cè)诎?mediasoup-broadcaster-demo 跑起來的時(shí)候,接收端看到的是那些花花綠綠的方塊的原因。mediasoup-broadcaster-demo 還提供了另外一個(gè)用于創(chuàng)建視頻的 Track 的接口, createVideoTrack(),這個(gè)接口創(chuàng)建的視頻源是 webrtc::FakePeriodicVideoTrackSource,它也沒有操作視頻采集設(shè)備,而是在內(nèi)存中構(gòu)造了一些畫面。

webrtc::FrameGeneratorCapturerVideoTrackSourcewebrtc::FakePeriodicVideoTrackSource 都是 webrtc 提供的測(cè)試基礎(chǔ)設(shè)施的一部分,它們可以生成一些視頻幀用于測(cè)試。

先來看 webrtc::FrameGeneratorCapturerVideoTrackSource。在 webrtc/api/test/frame_generator_interface.h 文件中定義了一個(gè)生成視頻幀數(shù)據(jù)的接口 FrameGeneratorInterface

namespace webrtc {
namespace test {

class FrameGeneratorInterface {
 public:
  struct VideoFrameData {
    VideoFrameData(rtc::scoped_refptr<VideoFrameBuffer> buffer,
                   absl::optional<VideoFrame::UpdateRect> update_rect)
        : buffer(std::move(buffer)), update_rect(update_rect) {}

    rtc::scoped_refptr<VideoFrameBuffer> buffer;
    absl::optional<VideoFrame::UpdateRect> update_rect;
  };

  enum class OutputType { kI420, kI420A, kI010, kNV12 };
  static const char* OutputTypeToString(OutputType type);

  virtual ~FrameGeneratorInterface() = default;

  // Returns VideoFrameBuffer and area where most of update was done to set them
  // on the VideoFrame object.
  virtual VideoFrameData NextFrame() = 0;

  // Change the capture resolution.
  virtual void ChangeResolution(size_t width, size_t height) = 0;
};

}  // namespace test
}  // namespace webrtc

webrtc/api/test/create_frame_generator.h 頭文件中聲明了許多創(chuàng)建 FrameGeneratorInterface 接口對(duì)象的函數(shù):

// Creates a frame generator that produces frames with small squares that
// move randomly towards the lower right corner.
// |type| has the default value FrameGeneratorInterface::OutputType::I420.
// |num_squares| has the default value 10.
std::unique_ptr<FrameGeneratorInterface> CreateSquareFrameGenerator(
    int width,
    int height,
    absl::optional<FrameGeneratorInterface::OutputType> type,
    absl::optional<int> num_squares);

// Creates a frame generator that repeatedly plays a set of yuv files.
// The frame_repeat_count determines how many times each frame is shown,
// with 1 = show each frame once, etc.
std::unique_ptr<FrameGeneratorInterface> CreateFromYuvFileFrameGenerator(
    std::vector<std::string> filenames,
    size_t width,
    size_t height,
    int frame_repeat_count);

// Creates a frame generator that repeatedly plays an ivf file.
std::unique_ptr<FrameGeneratorInterface> CreateFromIvfFileFrameGenerator(
    std::string filename);

// Creates a frame generator which takes a set of yuv files (wrapping a
// frame generator created by CreateFromYuvFile() above), but outputs frames
// that have been cropped to specified resolution: source_width/source_height
// is the size of the source images, target_width/target_height is the size of
// the cropped output. For each source image read, the cropped viewport will
// be scrolled top to bottom/left to right for scroll_tim_ms milliseconds.
// After that the image will stay in place for pause_time_ms milliseconds,
// and then this will be repeated with the next file from the input set.
std::unique_ptr<FrameGeneratorInterface>
CreateScrollingInputFromYuvFilesFrameGenerator(
    Clock* clock,
    std::vector<std::string> filenames,
    size_t source_width,
    size_t source_height,
    size_t target_width,
    size_t target_height,
    int64_t scroll_time_ms,
    int64_t pause_time_ms);

// Creates a frame generator that produces randomly generated slides. It fills
// the frames with randomly sized and colored squares.
// |frame_repeat_count| determines how many times each slide is shown.
std::unique_ptr<FrameGeneratorInterface>
CreateSlideFrameGenerator(int width, int height, int frame_repeat_count);

mediasoup-broadcaster-demo 中用到的 webrtc::FrameGeneratorCapturerVideoTrackSource 類用到了上面聲明的 CreateSquareFrameGenerator() 函數(shù)。webrtc/api/test/create_frame_generator.cc 文件中有這些函數(shù)的定義:

#include "test/frame_generator.h"
#include "test/testsupport/ivf_video_frame_generator.h"

namespace webrtc {
namespace test {

std::unique_ptr<FrameGeneratorInterface> CreateSquareFrameGenerator(
    int width,
    int height,
    absl::optional<FrameGeneratorInterface::OutputType> type,
    absl::optional<int> num_squares) {
  return std::make_unique<SquareGenerator>(
      width, height, type.value_or(FrameGeneratorInterface::OutputType::kI420),
      num_squares.value_or(10));
}

std::unique_ptr<FrameGeneratorInterface> CreateFromYuvFileFrameGenerator(
    std::vector<std::string> filenames,
    size_t width,
    size_t height,
    int frame_repeat_count) {
  RTC_DCHECK(!filenames.empty());
  std::vector<FILE*> files;
  for (const std::string& filename : filenames) {
    FILE* file = fopen(filename.c_str(), "rb");
    RTC_DCHECK(file != nullptr) << "Failed to open: '" << filename << "'\n";
    files.push_back(file);
  }

  return std::make_unique<YuvFileGenerator>(files, width, height,
                                            frame_repeat_count);
}

std::unique_ptr<FrameGeneratorInterface> CreateFromIvfFileFrameGenerator(
    std::string filename) {
  return std::make_unique<IvfVideoFrameGenerator>(std::move(filename));
}

std::unique_ptr<FrameGeneratorInterface>
CreateScrollingInputFromYuvFilesFrameGenerator(
    Clock* clock,
    std::vector<std::string> filenames,
    size_t source_width,
    size_t source_height,
    size_t target_width,
    size_t target_height,
    int64_t scroll_time_ms,
    int64_t pause_time_ms) {
  RTC_DCHECK(!filenames.empty());
  std::vector<FILE*> files;
  for (const std::string& filename : filenames) {
    FILE* file = fopen(filename.c_str(), "rb");
    RTC_DCHECK(file != nullptr);
    files.push_back(file);
  }

  return std::make_unique<ScrollingImageFrameGenerator>(
      clock, files, source_width, source_height, target_width, target_height,
      scroll_time_ms, pause_time_ms);
}

std::unique_ptr<FrameGeneratorInterface>
CreateSlideFrameGenerator(int width, int height, int frame_repeat_count) {
  return std::make_unique<SlideGenerator>(width, height, frame_repeat_count);
}

}  // namespace test
}  // namespace webrtc

webrtc/api/test/create_frame_generator.cc 文件中的這些函數(shù)創(chuàng)建的實(shí)際實(shí)現(xiàn)了 FrameGeneratorInterface 接口的類的對(duì)象,在 webrtc/test/frame_generator.h 中聲明:

// SquareGenerator is a FrameGenerator that draws a given amount of randomly
// sized and colored squares. Between each new generated frame, the squares
// are moved slightly towards the lower right corner.
class SquareGenerator : public FrameGeneratorInterface {
 public:
  SquareGenerator(int width, int height, OutputType type, int num_squares);

  void ChangeResolution(size_t width, size_t height) override;
  VideoFrameData NextFrame() override;

 private:
  rtc::scoped_refptr<I420Buffer> CreateI420Buffer(int width, int height);

  class Square {
   public:
    Square(int width, int height, int seed);

    void Draw(const rtc::scoped_refptr<VideoFrameBuffer>& frame_buffer);

   private:
    Random random_generator_;
    int x_;
    int y_;
    const int length_;
    const uint8_t yuv_y_;
    const uint8_t yuv_u_;
    const uint8_t yuv_v_;
    const uint8_t yuv_a_;
  };

  Mutex mutex_;
  const OutputType type_;
  int width_ RTC_GUARDED_BY(&mutex_);
  int height_ RTC_GUARDED_BY(&mutex_);
  std::vector<std::unique_ptr<Square>> squares_ RTC_GUARDED_BY(&mutex_);
};

class YuvFileGenerator : public FrameGeneratorInterface {
 public:
  YuvFileGenerator(std::vector<FILE*> files,
                   size_t width,
                   size_t height,
                   int frame_repeat_count);

  ~YuvFileGenerator();

  VideoFrameData NextFrame() override;
  void ChangeResolution(size_t width, size_t height) override {
    RTC_NOTREACHED();
  }

 private:
  // Returns true if the new frame was loaded.
  // False only in case of a single file with a single frame in it.
  bool ReadNextFrame();

  size_t file_index_;
  size_t frame_index_;
  const std::vector<FILE*> files_;
  const size_t width_;
  const size_t height_;
  const size_t frame_size_;
  const std::unique_ptr<uint8_t[]> frame_buffer_;
  const int frame_display_count_;
  int current_display_count_;
  rtc::scoped_refptr<I420Buffer> last_read_buffer_;
};

// SlideGenerator works similarly to YuvFileGenerator but it fills the frames
// with randomly sized and colored squares instead of reading their content
// from files.
class SlideGenerator : public FrameGeneratorInterface {
 public:
  SlideGenerator(int width, int height, int frame_repeat_count);

  VideoFrameData NextFrame() override;
  void ChangeResolution(size_t width, size_t height) override {
    RTC_NOTREACHED();
  }

 private:
  // Generates some randomly sized and colored squares scattered
  // over the frame.
  void GenerateNewFrame();

  const int width_;
  const int height_;
  const int frame_display_count_;
  int current_display_count_;
  Random random_generator_;
  rtc::scoped_refptr<I420Buffer> buffer_;
};

class ScrollingImageFrameGenerator : public FrameGeneratorInterface {
 public:
  ScrollingImageFrameGenerator(Clock* clock,
                               const std::vector<FILE*>& files,
                               size_t source_width,
                               size_t source_height,
                               size_t target_width,
                               size_t target_height,
                               int64_t scroll_time_ms,
                               int64_t pause_time_ms);
  ~ScrollingImageFrameGenerator() override = default;

  VideoFrameData NextFrame() override;
  void ChangeResolution(size_t width, size_t height) override {
    RTC_NOTREACHED();
  }

 private:
  void UpdateSourceFrame(size_t frame_num);
  void CropSourceToScrolledImage(double scroll_factor);

  Clock* const clock_;
  const int64_t start_time_;
  const int64_t scroll_time_;
  const int64_t pause_time_;
  const size_t num_frames_;
  const int target_width_;
  const int target_height_;

  size_t current_frame_num_;
  bool prev_frame_not_scrolled_;
  VideoFrameData current_source_frame_;
  VideoFrameData current_frame_;
  YuvFileGenerator file_generator_;
};

}  // namespace test
}  // namespace webrtc

上面這些類在 webrtc/test/frame_generator.cc 文件中定義。

mediasoup-broadcaster-demo 中用到的 FrameGeneratorCapturerVideoTrackSource 在文件 webrtc/pc/test/frame_generator_capturer_video_track_source.h 中定義:

namespace webrtc {

// Implements a VideoTrackSourceInterface to be used for creating VideoTracks.
// The video source is generated using a FrameGeneratorCapturer, specifically
// a SquareGenerator that generates frames with randomly sized and colored
// squares.
class FrameGeneratorCapturerVideoTrackSource : public VideoTrackSource {
 public:
  static const int kDefaultFramesPerSecond = 30;
  static const int kDefaultWidth = 640;
  static const int kDefaultHeight = 480;
  static const int kNumSquaresGenerated = 50;

  struct Config {
    int frames_per_second = kDefaultFramesPerSecond;
    int width = kDefaultWidth;
    int height = kDefaultHeight;
    int num_squares_generated = 50;
  };

  FrameGeneratorCapturerVideoTrackSource(Config config,
                                         Clock* clock,
                                         bool is_screencast)
      : VideoTrackSource(false /* remote */),
        task_queue_factory_(CreateDefaultTaskQueueFactory()),
        is_screencast_(is_screencast) {
    video_capturer_ = std::make_unique<test::FrameGeneratorCapturer>(
        clock,
        test::CreateSquareFrameGenerator(config.width, config.height,
                                         absl::nullopt,
                                         config.num_squares_generated),
        config.frames_per_second, *task_queue_factory_);
    video_capturer_->Init();
  }

  FrameGeneratorCapturerVideoTrackSource(
      std::unique_ptr<test::FrameGeneratorCapturer> video_capturer,
      bool is_screencast)
      : VideoTrackSource(false /* remote */),
        video_capturer_(std::move(video_capturer)),
        is_screencast_(is_screencast) {}

  ~FrameGeneratorCapturerVideoTrackSource() = default;

  void Start() { SetState(kLive); }

  void Stop() { SetState(kMuted); }

  bool is_screencast() const override { return is_screencast_; }

 protected:
  rtc::VideoSourceInterface<VideoFrame>* source() override {
    return video_capturer_.get();
  }

 private:
  const std::unique_ptr<TaskQueueFactory> task_queue_factory_;
  std::unique_ptr<test::FrameGeneratorCapturer> video_capturer_;
  const bool is_screencast_;
};

}  // namespace webrtc

FrameGeneratorCapturerVideoTrackSource 包了一個(gè) FrameGeneratorCapturer,后者包了一個(gè) FrameGeneratorInterfaceFrameGeneratorCapturerwebrtc/test/frame_generator_capturer.hwebrtc/test/frame_generator_capturer.cc 中聲明和定義。相關(guān)的這些組件的關(guān)系如下圖:

1646735205460.jpg

接下來來看 mediasoup-broadcaster-demo 中用到的另一個(gè)測(cè)試基礎(chǔ)設(shè)施, FakePeriodicVideoTrackSource,它在 webrtc/pc/test/fake_periodic_video_track_source.h 文件中定義:

namespace webrtc {

// A VideoTrackSource generating frames with configured size and frame interval.
class FakePeriodicVideoTrackSource : public VideoTrackSource {
 public:
  explicit FakePeriodicVideoTrackSource(bool remote)
      : FakePeriodicVideoTrackSource(FakePeriodicVideoSource::Config(),
                                     remote) {}

  FakePeriodicVideoTrackSource(FakePeriodicVideoSource::Config config,
                               bool remote)
      : VideoTrackSource(remote), source_(config) {}

  ~FakePeriodicVideoTrackSource() = default;

  const FakePeriodicVideoSource& fake_periodic_source() const {
    return source_;
  }

 protected:
  rtc::VideoSourceInterface<VideoFrame>* source() override { return &source_; }

 private:
  FakePeriodicVideoSource source_;
};

}  // namespace webrtc

FakePeriodicVideoTrackSource 封裝了 FakePeriodicVideoSource,而后者則在 webrtc/pc/test/fake_periodic_video_source.h 中定義:

namespace webrtc {

class FakePeriodicVideoSource final
    : public rtc::VideoSourceInterface<VideoFrame> {
 public:
  static constexpr int kDefaultFrameIntervalMs = 33;
  static constexpr int kDefaultWidth = 640;
  static constexpr int kDefaultHeight = 480;

  struct Config {
    int width = kDefaultWidth;
    int height = kDefaultHeight;
    int frame_interval_ms = kDefaultFrameIntervalMs;
    VideoRotation rotation = kVideoRotation_0;
    int64_t timestamp_offset_ms = 0;
  };

  FakePeriodicVideoSource() : FakePeriodicVideoSource(Config()) {}
  explicit FakePeriodicVideoSource(Config config)
      : frame_source_(
            config.width,
            config.height,
            config.frame_interval_ms * rtc::kNumMicrosecsPerMillisec,
            config.timestamp_offset_ms * rtc::kNumMicrosecsPerMillisec),
        task_queue_(std::make_unique<TaskQueueForTest>(
            "FakePeriodicVideoTrackSource")) {
    thread_checker_.Detach();
    frame_source_.SetRotation(config.rotation);

    TimeDelta frame_interval = TimeDelta::Millis(config.frame_interval_ms);
    RepeatingTaskHandle::Start(task_queue_->Get(), [this, frame_interval] {
      if (broadcaster_.wants().rotation_applied) {
        broadcaster_.OnFrame(frame_source_.GetFrameRotationApplied());
      } else {
        broadcaster_.OnFrame(frame_source_.GetFrame());
      }
      return frame_interval;
    });
  }

  rtc::VideoSinkWants wants() const {
    MutexLock lock(&mutex_);
    return wants_;
  }

  void RemoveSink(rtc::VideoSinkInterface<webrtc::VideoFrame>* sink) override {
    RTC_DCHECK(thread_checker_.IsCurrent());
    broadcaster_.RemoveSink(sink);
  }

  void AddOrUpdateSink(rtc::VideoSinkInterface<webrtc::VideoFrame>* sink,
                       const rtc::VideoSinkWants& wants) override {
    RTC_DCHECK(thread_checker_.IsCurrent());
    {
      MutexLock lock(&mutex_);
      wants_ = wants;
    }
    broadcaster_.AddOrUpdateSink(sink, wants);
  }

  void Stop() {
    RTC_DCHECK(task_queue_);
    task_queue_.reset();
  }

 private:
  rtc::ThreadChecker thread_checker_;

  rtc::VideoBroadcaster broadcaster_;
  cricket::FakeFrameSource frame_source_;
  mutable Mutex mutex_;
  rtc::VideoSinkWants wants_ RTC_GUARDED_BY(&mutex_);

  std::unique_ptr<TaskQueueForTest> task_queue_;
};

FakePeriodicVideoSource 是對(duì) FakeFrameSource 的封裝,FakeFrameSource 在文件webrtc/media/base/fake_frame_source.h 中定義:

namespace cricket {

class FakeFrameSource {
 public:
  FakeFrameSource(int width,
                  int height,
                  int interval_us,
                  int64_t timestamp_offset_us);
  FakeFrameSource(int width, int height, int interval_us);

  webrtc::VideoRotation GetRotation() const;
  void SetRotation(webrtc::VideoRotation rotation);

  webrtc::VideoFrame GetFrame();
  webrtc::VideoFrame GetFrameRotationApplied();

  // Override configuration.
  webrtc::VideoFrame GetFrame(int width,
                              int height,
                              webrtc::VideoRotation rotation,
                              int interval_us);

 private:
  const int width_;
  const int height_;
  const int interval_us_;

  webrtc::VideoRotation rotation_ = webrtc::kVideoRotation_0;
  int64_t next_timestamp_us_;
};

}  // namespace cricket

并在文件 webrtc/media/base/fake_frame_source.cc 中實(shí)現(xiàn)。FakeFrameSource 在內(nèi)存中創(chuàng)建一些視頻幀。

相關(guān)的這些組件之間的關(guān)系大概如下圖:

1646737630600.jpg

關(guān)于 mediasoup-broadcaster-demo 的更多內(nèi)容可參考 mediasoup-broadcaster-demo。此外,筆者 fork 了這個(gè) repo mediasoup-broadcaster-demo,會(huì)針對(duì)遇到的問題做一些修改。

參考資料

Npm can't find module "semver" error in Ubuntu 19.04
mediasoup-demo 實(shí)踐
https://github.com/versatica/mediasoup-demo/blob/v3/README.md
https://mediasoup.discourse.group/t/mediasouperror-port-bind-failed-due-to-address-not-available-udp-1-2-3-4-attempt-1/32/6
https://github.com/mkhahani/mediasoup-sample-app/issues/1
https://stackoverflow.com/questions/7724569/debug-vs-release-in-cmake

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容