(圖片來源:李超)node
mediasoup
的服務端由兩部分構成:
一、使用 C++
編寫的做爲子進程的媒體層 (ICE
, DTLS
, RTP
等)。可執行文件在 Linux
或 macOS
上爲 mediasoup-worker
,在 Windows
上爲 mediasoup-worker.exe
。
二、使用 Javascript
(Typescript
) 編寫的、基於 Node.js
的用於與 mediasoup-worker 進行通訊的組件。由於官方或幾乎全部第三方的 mediasoup 服務端都是使用的是 Node.js 來實現,因此官方提供一箇中間層讓開發者不直接和 mediassoup-workder 交互。linux
本文主要討論如何使用 ASP.NET Core
替換 Javascript(Node.js) 的實現。nginx
(備註:因爲是在參考圖基礎上 PS 的,不太準確,有心情了再改吧。)git
libuv
和 V8
是 Node.js 的基石,而 mediasoup-worker 也使用了 libuv。
在 Node.js 程序中,安裝 mediasoup 的模塊時會將 mediasoup-worker 會自動編譯在 node_modules
裏。能夠直接將 mediasoup-worker 拷貝出來在 Shell 中運行——固然,一運行就會退出。github
> ./mediasoup-worker mediasoup-worker::main() | you don't seem to be my real father!
經過查看 mediasoup-worker 的源碼得知其須要一個 MEDIASOUP_VERSION
環境變量——固然,加上後一運行仍是會退出。web
> MEDIASOUP_VERSION=3.5.5 ./mediasoup-worker UnixStreamSocket::UnixStreamSocket() | throwing MediaSoupError: uv_pipe_open() failed: inappropriate ioctl for device mediasoup-worker::main() | error creating the Channel: uv_pipe_open() failed: inappropriate ioctl for device
緣由是 mediasoup-worker 依賴於兩個目前並不存在的文件描述符 3 和 4。這裏的 3 和 4 實際上是一種約定。那在 Shell 中重定向到標準輸出試試。api
> MEDIASOUP_VERSION=3.5.5 ./mediasoup-worker 3>&1 4>&1 37:{"event":"running","targetId":"3574"},
可以獲取到 mediasroup-worker 啓動成功後的輸出。瀏覽器
在 Linux 上,在 fork 子進程的時候,會將父進程的文件描述符傳遞到子進程中,這是進程間通訊的一種方式。Node.js 程序 fork
進程以前,會建立幾個 libuv 概念下而非 Linux 概念下的抽象意義上的 pipe
,在 Linux 中使用的是 Unix Domain Socket 實現。Node.js 程序或者說 libuv fork 進程後,會在子進程將要使用的文件描述符重定向。好比在父進程,指望子進程持有的文件描述符是 3 和 4 而其實是 11 和 13,fork 以後仍是 11 和 13 ,在子進程中使用 fcntl
系統調用重定向。經過合理的數量和順序上的約定能肯定重定向爲 3 和 4 。最終在子進程中 exec
mediasoup-worker(見:uv__process_child_init)。服務器
// File: node_modules/mediasoup/src/Worker.ts this._child = spawn( // command spawnBin, // args spawnArgs, // options { env : { MEDIASOUP_VERSION : '__MEDIASOUP_VERSION__' }, detached : false, // fd 0 (stdin) : Just ignore it. // fd 1 (stdout) : Pipe it for 3rd libraries that log their own stuff. // fd 2 (stderr) : Same as stdout. // fd 3 (channel) : Producer Channel fd. // fd 4 (channel) : Consumer Channel fd. stdio : [ 'ignore', 'pipe', 'pipe', 'pipe', 'pipe' ] } );
參考:Node.js 的 spawn 和 libuv 的 uv_spawn 的實現源碼,以及 mediasoup 的 Node.js 模塊的源碼。websocket
備註:libuv 在 Windows 上進程間通訊使用的是命名管道(Named Pipe)。
下面是使用 C 語言實現的一個很是粗糙的版本。
// // main.c // TestMedaisoup // // Created by Alby on 2020/3/31. // Copyright © 2020 alby. All rights reserved. // #include <stdio.h> #include <uv.h> #define ASSERT(expr) \ do { \ if (!(expr)) { \ fprintf(stderr, \ "Assertion failed in %s on line %d: %s\n", \ __FILE__, \ __LINE__, \ #expr); \ abort(); \ } \ } while (0) static int close_cb_called; static int exit_cb_called; static uv_process_t process; static uv_process_options_t options; static char* args[5]; #define OUTPUT_SIZE 1024 static char output[OUTPUT_SIZE]; static int output_used; static void init_process_options(char* test, uv_exit_cb exit_cb) { char *exepath = "/Users/XXXX/Developer/OpenSource/Meeting/Lab/worker/mediasoup-worker"; args[0] = exepath; args[1] = NULL; args[2] = NULL; args[3] = NULL; args[4] = NULL; options.file = exepath; options.args = args; options.exit_cb = exit_cb; options.flags = 0; } static void close_cb(uv_handle_t* handle) { printf("close_cb\n"); close_cb_called++; } static void exit_cb(uv_process_t* process, int64_t exit_status, int term_signal) { printf("exit_cb\n"); exit_cb_called++; ASSERT(exit_status == 1); ASSERT(term_signal == 0); uv_close((uv_handle_t*)process, close_cb); } static void on_alloc(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { buf->base = output + output_used; buf->len = OUTPUT_SIZE - output_used; } static void on_read(uv_stream_t* tcp, ssize_t nread, const uv_buf_t* buf) { if (nread > 0) { output_used += nread; printf(buf->base); } else if (nread < 0) { ASSERT(nread == UV_EOF); uv_close((uv_handle_t*)tcp, close_cb); } } int main() { const int stdio_count = 5; int r; uv_pipe_t pipes[4]; uv_stdio_container_t stdio[5]; init_process_options("spawn_helper5", exit_cb); for(int i = 1; i < stdio_count; i++) { uv_pipe_init(uv_default_loop(), &pipes[i-1], 0); } stdio[0].flags = UV_IGNORE; for(int i = 1; i < stdio_count; i++) { stdio[i].flags = UV_CREATE_PIPE | UV_READABLE_PIPE | UV_WRITABLE_PIPE; stdio[i].data.stream = (uv_stream_t*)&pipes[i-1]; } char* quoted_path_env[1]; quoted_path_env[0] = "MEDIASOUP_VERSION=3.5.5"; options.env = quoted_path_env; options.stdio = stdio; options.stdio_count = stdio_count; r = uv_spawn(uv_default_loop(), &process, &options); ASSERT(r == 0); for(int i = 1; i < stdio_count; i++) { r = uv_read_start((uv_stream_t*) &pipes[i - 1], on_alloc, on_read); ASSERT(r == 0); } r = uv_run(uv_default_loop(), UV_RUN_DEFAULT); ASSERT(r == 0); ASSERT(exit_cb_called == 1); ASSERT(close_cb_called == 5); /* Once for process once for the pipe. */ return 0; }
咱們一般在 .Net
中使用 Process
類建立子進程,而 Process 類知足不了需求而且直接使用 Win32
的 CreateProcess
將問題複雜化了。我決定使用 Libuv——幸虧微軟提供了一個 Libuv 的 Nuget
包,支持 Linux、macOS 和 Windows;其次 LibuvSharp
提供了 P/Invoker
實現。
下面是 C# 版的 spawn
, 看起來沒有 Node.js 版那麼簡潔,可是功能徹底同樣:
// ...... _pipes = new Pipe[StdioCount]; // 備註:忽略標準輸入 for (var i = 1; i < StdioCount; i++) { _pipes[i] = new Pipe() { Writeable = true, Readable = true }; } try { // 備註:和 Node.js 不一樣,_child 沒有 error 事件。不過,Process.Spawn 可拋出異常。 _child = Process.Spawn(new ProcessOptions() { File = mediasoupOptions.WorkerPath, Arguments = args.ToArray(), Environment = env, Detached = false, Streams = _pipes, }, OnExit); ProcessId = _child.Id; } catch (Exception ex) { _child = null; Close(); if (!_spawnDone) { _spawnDone = true; _logger.LogError($"Worker() | worker process failed [pid:{ProcessId}]: {ex.Message}"); Emit("@failure", ex); } else { // 執行到這裏的可能性? _logger.LogError($"Worker() | worker process error [pid:{ProcessId}]: {ex.Message}"); Emit("died", ex); } } // ......
備註:LibuvSharp 原版有個小 bug。 (uv_process_t*)(NativeHandle.ToInt32() + Handle.Size(HandleType.UV_HANDLE));
須要改成 (uv_process_t*)(NativeHandle.ToInt64() + Handle.Size(HandleType.UV_HANDLE));
。另外要使用 Pipe
建立管道而不是看起來更像的 IPCPipe
——我被坑得很慘。
一般,在瀏覽器使用 WebSocket
組件而不是原生 WebSocket 對開發者來講更友好。 Node.js 版經常使用的是 socket.io, mediasoup 官方 Demo 使用的是 protoo , 而在 ASP.NET Core 下,使用 SignalR 是更好的選擇。在改寫的過程當中發現服務端向客戶端發送數據不支持返回值, 不過這個能夠準備一個服務端方法供客戶端調用來解決。
備註: 在從新實現了服務端的狀況下,相應的客戶端也須要配合調整,這意味着無法使用官方的客戶端。
Talk is cheap:
在本機運行延遲是 70ms 左右, 效果圖(圖左是本地視頻,圖右是遠程視頻):
在外網服務器運行 multiparty-meeting 這個非官方 Demo 的延遲是 160ms 左右,效果圖(圖上是本地視頻,圖下是遠程視頻):
mediasoup
multiparty-meeting
nodejs
libuv
libuv-build
LibuvSharp
How to: Use Named Pipes for Network Interprocess Communication
UnixDomainSocketEndPoint Class
How to connect to a Unix Domain Socket in .NET Core in C#
Unix: Why not use Unix Domain Sockets for Named Pipes?
Serving .NET Core apps on Linux with nginx and Kestrel
Introduction to ASP.NET Core SignalR
基於mediasoup的多方通話研究(一)
多人實時互動之各WebRTC流媒體服務器比較