使用 ASP.NET Core 作為 mediasoup 的信令服務器


一、概述


(圖片來源:李超)

mediasoup 的服務端由兩部分構成:
1、使用 C++ 編寫的作為子進程的媒體層 (ICE, DTLS, RTP 等)。可執行文件在 LinuxmacOS 上為 mediasoup-worker,在 Windows 上為 mediasoup-worker.exe
2、使用 Javascript(Typescript) 編寫的、基於 Node.js 的用於與 mediasoup-worker 進行通信的組件。因為官方或幾乎所有第三方的 mediasoup 服務端都是使用的是 Node.js 來實現,所以官方提供一個中間層讓開發者不直接和 mediassoup-workder 交互。

本文主要討論如何使用 ASP.NET Core 替換 Javascript(Node.js) 的實現。


(備注:由於是在參考圖基礎上 PS 的,不太准確,有心情了再改吧。)

二、進程及進程間通信:Node.js 版

1、Node.js 的 spawn 和 libuv uv_spawn(fork/exec)

libuvV8 是 Node.js 的基石,而 mediasoup-worker 也使用了 libuv。
在 Node.js 程序中,安裝 mediasoup 的模塊時會將 mediasoup-worker 會自動編譯在 node_modules 里。可以直接將 mediasoup-worker 拷貝出來在 Shell 中運行——當然,一運行就會退出。

> ./mediasoup-worker
mediasoup-worker::main() | you don't seem to be my real father!

通過查看 mediasoup-worker 的源碼得知其需要一個 MEDIASOUP_VERSION 環境變量——當然,加上后一運行還是會退出。

> 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 中重定向到標准輸出試試。

> 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 模塊的源碼。

備注:libuv 在 Windows 上進程間通信使用的是命名管道(Named Pipe)。

2、C 實現

下面是使用 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;
}

三、進程及進程間通信:ASP.NET Core 版

我們通常在 .Net 中使用 Process 類創建子進程,而 Process 類滿足不了需求並且直接使用 Win32CreateProcess 將問題復雜化了。我決定使用 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:使用 SignalR 替代 protoo 或 socket.io

通常,在瀏覽器使用 WebSocket 組件而不是原生 WebSocket 對開發者來說更友好。 Node.js 版常用的是 socket.io, mediasoup 官方 Demo 使用的是 protoo , 而在 ASP.NET Core 下,使用 SignalR 是更好的選擇。在改寫的過程中發現服務端向客戶端發送數據不支持返回值, 不過這個可以准備一個服務端方法供客戶端調用來解決。

備注: 在重新實現了服務端的情況下,相應的客戶端也需要配合調整,這意味着沒法使用官方的客戶端。

五、ASP.NET Core 實現

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流媒體服務器比較


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM