6

使用 ASP.NET Core 实现 mediasoup 的信令服务器

 3 years ago
source link: http://blog.tubumu.com/2020/05/05/mediasoup-01/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
001.jpg
(图片来源:李超)

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) 的实现。

002.jpg

(备注:由于是在参考图基础上 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 中运行——当然,一运行就会退出。

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

通过查看 mediasoup-worker 的源码得知其需要一个 MEDIASOUP_VERSION 环境变量——当然,加上后一运行还是会退出。

1
2
3
> 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 中重定向到标准输出试试。

1
2
> 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)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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 语言实现的一个非常粗糙的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//
// 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 版那么简洁,但是功能完全一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// ......
_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(图左是 Node.js 实现,图右是 ASP.NET Core 实现):

003.jpg

在本机运行延迟是 130ms 左右, 效果图(图左是本地视频,图右是远程视频):

004.jpg

在外网服务器运行 multiparty-meeting 这个非官方 Demo 的延迟是 160ms 左右,效果图(图上是本地视频,图下是远程视频):

005.jpg

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流媒体服务器比较


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK