一、概述
Mediasoup
主要提供了 3 个库和 1 个 demo。
库名 | 说明 |
---|---|
mediasoup | 主要包含三部分。一是 worker 可执行程序,由 C++ 实现,是本系列分析的重点;二是 Node 库,由 TypeScript 实现;三是 Rust 库,和 Node 的主要不同在于它没有以进程方式而是以静态库方式使用 mediasoup-worker。 |
mediasoup-client | Web 客户端库。TypeScript 实现。 |
libmediasoupclient | Native 客户端库。C++ 实现。 |
mediasoup-demo | 官方 Demo。 |
Examples | 各种示例。 |
网络上对 mediasoup 的 Node.js
层——准确说是对官方的 mediasoup-demo
的源码分析比较多,对于 mediasoup-client
和 mediasoup-worker
(之后简 worker) 等的源码详细分析相对较少。本人之前有将 GB28281
集成进 mediasoup 的想法并验证了可行性,以及使用 .Net
重新实现过 Node.js 层(含 mediasoup-client 和 mediasoup-demo),对 worker 的源码进行过比较粗略地浏览。最近基于想要弥补一些比较模糊的认知,并且 mediaoup 本身也在进化,故就再做了一次源码的梳理。
至于 mediasoup 是什么、能做什么、与其他 SFU 相较而言的优缺点、Demo 如何运行、为什么不用单一语言来实现等等讨论不是本系列关注的重点。
二、名词/概念
mediasoup 定义和抽象了一些名词和概念,为了方便描述,做整理如下表:
名词/概念 | 说明 |
---|---|
Settings | 设置。用于读取命令行配置、将配置输出到日志,以及运行时更新配置。另外用 线程本地存储 保存配置以确保每个线程都有一个 Settings 配置对象。当然,以 worker 进程方式运行不存在这个问题。设置的选项(option: getopt.h)具体包括:logLevel 、logTags 、rtcMinPort 和 rtcMaxPort 、dtlsCertificateFile 和 dtlsPrivateKeyFile 这六项,分别表示日志等级、日志标签(如ice、dtls等,控制哪些类的日志才会传出)、最小和最大 rtc 端口,以及 dtls 证书文件和私钥的物理路径。这些参数都是可选的,如果未提供则会使用默认值或自动生成。 |
Logger | 日志。有 debug 、warn 、error 和 none 四个等级。日志通过 ChannelSocket 传输到父进程,如果 ChannelSocket 尚未创建则会在标准输出中打印日志。mediasoup 给很多方法都加了 Trace 日志,打开 MS_LOG_TRACE 编译宏开关并且设置日志等级为 debug 则会输出。类似于 Settings,Logger 也用线程本地存储保存 Buffer 和 ChannelSocket 以确保每个线程都有一个对象。 |
ChannelSocket | 通道 Socket。抽象了 worker 进程和父进程的通信。得益于 libuv ,对于进程间通信开发者不用关心操作系统是用的 Linux/UNIX 的 UNIX Domain Socket 还是 Windows 的 IPC 等。而 mediasoup 的这层 ChannelSocket 抽象,也为后来支持的 以库的方式使用 worker 打下基础。ChannelSocket 是双向的,主要向父进程传输日志;接受父进程的如创建 Transport 、Producer 等指令请求;向父进程传输指令请求的执行结果;向父进程传输 Producer 暂停关闭、SCTP 发送 Buffer 已满之类的通知。所有消息都是基于文本的。 |
PayloadChannelSocket | 负载通道 Socket。类似于 ChannelSocket,不同的是 PayloadChannelSocket 用于 DirectTransport 向父进程发送 RTP 或 RTCP 数据包等。消息是基于文本和二进制的。 |
Worker | 工作者。注意这里并非指 worker 进程,但某种意义下可以指代 worker 进程。ChannelSocket 和 PayloadChannelSocket 只负责消息的接收和发送,而 Worker 是具体消息的处理入口。它根据消息的类别进行处理,对于自身可以负责的请求比如获取资源使用率、创建及关闭 Router 、dump 获取本 Worker 下的 Router 的 Id 集合等则直接处理,其余的就交给 Router 来处理。遵循谁创建谁销毁的原则,Router 的销毁工作也由 Worker 负责。 |
Router | 路由。Router 是比较重要的概念,不准确地说可以将 Router 和房间对应起来。其保存了 Transport、Producer、Consumer 、Producer下对应的 Consumer 以及 DataProducer 和 DataConsumer 相关的集合。如上文所述,Worker 将其处理不了的工作交给 Router,Router 能够处创建和关闭 Transport 以及和 RtpObserver 相关的几个操作请求,其余的就交给 Transport 处理。另外,Router 只负责 Transport 和 RtpObserver 的销毁工作,对于 Producer、Consumer 等则由创建这些对象的 Transport 负责。 |
Transport | 传输通道。Transport 是抽象类,具体类包括:WebRtcTransport 、PlainTransport 、DirectTransport 和 PipeTransport 。 |
WebRtcTransport | WebRtc 传输通道。WebRtcTransport 包含 Socket 服务端,将接收到的数据包尝试转换为 StunPacket 、Rtcp Packet 、RtpPacket 或 Dtls 数据 中的一种。StunPacket 由 IceServer 处理,Rtcp Packet 会经过处理后发送给 Consumer, RtpPacket 由相应的 Producer 处理,Dtls 数据当然是先解密再处理;也会通过 Sokcet 将数据发送到本 Transport 下对应的 Consumer 中去。 |
PlainTransport | Plain 传输通道。类似于 WebRtcTransport,不同的是不会收到 StunPacket 。 |
PipeTransport | Pipe 传输通道。用于跨 Router/worker Rtp 包传输。 |
DirectTransport | Direct 传输通道。DirectTransport 不包含 Socket 服务端,将接收到的数据直接通过 PayloadChannel 发送给 Consumer。 |
Producer | 生产者。类型有四类:SIMPLE、SIMULCAST、SVC 和 PIPE。 |
Consumer | 消费者。Consumer 是抽象类,具体类包括:SimpleConsumer 、SimulcastConsumer 、SvcConsumer 和 PipeConsumer 。如果是 PipeTranspor 进行消费,则 type 为 “pipe”,否则为 Producer 的 type。 |
SimpleConsumer | 单体消费者。 |
SimulcastConsumer | Simulcast 消费者。 |
SvcConsumer | Svc 消费者。 |
PipeConsumer | Pipe 消费者。 |
DataProducer | Data 生产者。类型有两类:SCTP 和 DIRECT。只有传递 enableSctp 为 true 的参数才能创建类型为 SCTP 的 DataProducer;只能在 DirectTransport 之上创建类型为 DIRECT 的 DataProducer。 |
DataConsumer | Data 消费者。只有传递 enableSctp 为 true 的参数才能创建类型为 SCTP 的 DataProducer;只能在 DirectTransport 之上创建类型为 DIRECT 的 DataConsumer。 |
RtpObserver | Rtp 观察者。RtpObserver 是抽象类,具体类包括:AudioLevelObserver 和 ActiveSpeakerObserver 。 |
AudioLevelObserver | 音量观察者。收到音频 Rtp 包后累计音量。比如每1秒间隔计算触发一次计算,如果对应 Producer 收集了 10 个及以上的包则计算音频的平均值,如果值大于默认 -80dB 则将 Producder 的 Id 收集起来,最多收集指定参数 maxEntries 那么多个。如果存在符合条件的 Producer 则发出 volumes 事件,否则发出 silence 事件。事件会发送给父进程,而父进程会发送给相应的客户端。 |
ActiveSpeakerObserver | 说话人观察者。ActiveSpeakerObserver 是 3.8.0 新增的。具体算法不算太简单,大致看并不是谁说话就算,而是计算出会议中说话人中谁是主导者。如果存在符合条件的唯一 Producer 则发出 dominantspeaker 事件。事件会发送给父进程,而父进程会发送给相应的客户端。 |
父对象通常会监听子对象的相关事件,比如 Worker 会监听 ChannelSocket 和 PayloadChannelSocket 的相关事件(Router没有监听器),Router 会监听 Transport 的相关事件, Transport 会监听 Producer 、 Consumer 等的相关事件。
如果要扩展其他协议,比如支持 GB28181 的 PS 流,可以创建自定义 Transport 。
如果要集成一些 AI 功能,比如目标识别,手势识别,语音指令识别等,可以创建自定义 RtpObserver 。
三、ChannelSocket 消息类型
ChannelRequest::MethodId
枚举定义了各种消息。其命名有一定规律,除 *.close
和 RtpObServer
相关的消息外,第一个下划线前的第一个单词通常标明了消息由谁来处理。比如 TRANSPORT_PRODUCE
,表示 Transport 需要处理 Produce 消息。
1 | // File: worker/src/Channel/ChannelRequest.cpp |
四、PayloadChannelSocket 消息类型
PayloadChannelRequest::MethodId
枚举定义一种消息。
1 | // File: worker/src/PayloadChannel/PayloadChannelRequest.cpp |
五、ChannelSocket 通知类型
为了方便使用, mediasoup 定义了 ChannelNotifier
类用于 worker 向父进程发送通知。通知类型整理如下:
Sender | 类型 | 说明 |
---|---|---|
Workder | running | worker 运行中。 |
ActiveSpeakerObserver | dominantspeaker | 当前主导发言者。 |
AudioLevelObserver | volumes | 当前发言者的音量。 |
- | silence | 之前发言者已静音。 |
Consumer | producerpause | 生产者已暂停。 |
- | producerresume | 生产者已恢复。 |
- | producerclose | 生产者已关闭 |
- | trace | 跟踪消费者信息。类型包括:rtp、keyframe、nack、pli 和 fir。 |
DataConsumer | bufferedamountlow | 数据消费者 Sctp Buffer 低。 |
- | dataproducerclose | 数据生产者已关闭。 |
PlainTransport | tuple | TransportTuple。 当 comedia 为 true 时,通过收到的数据提取到 TransportTuple。 |
- | rtcptuple | TransportTuple。 当 comedia 为 true 且 rtcpMux 为 false, 通过收到的数据提取到 TransportTuple。 |
Producer | videoorientationchange | 视频方向发生改变。 |
- | score | Rtp 流的质量评分。 |
- | trace | 跟踪生产者信息。类型包括:rtp、keyframe、nack、pli 和 fir。 |
SctpAssociation | sctpsendbufferfull | DataConsumer 的 Sctp 发送 Buffer 已满。 |
SimpleConsumer | score | Rtp 流的质量评分。 |
SimulcastConsumer | score | Rtp 流的质量评分。包含自身的和 Producer 的。 |
- | layerschange | Layers 已改变。 |
SvcConsumer | score | Rtp 流的质量评分。包含自身的和 Producer 的。 |
- | layerschange | Layers 已改变。 |
Transport | trace | 跟踪 Transport 信息。类型包括:probation 和 bwe。 |
- | sctpstatechange | Sctp 状态已改变。 |
WebRtcTransport | iceselectedtuplechange | Ice 选择的 TransportTuple 已改变。 |
- | icestatechange | Ice 状态已改变。 |
- | dtlsstatechange | Dtls 状态已改变。 状态包括:connecting、connected、completed、disconnected、closed 和 failed。 |
六、PayloadChannelSocket 通知类型
为了方便使用, mediasoup 定义了 PayloadChannelNotifier
类用于 worker 向父进程发送通知。通知类型整理如下:
Sender | 类型 | 说明 |
---|---|---|
DirectTransport | rtp | Rtp 包。 |
- | rtcp | Rtcp 包。 |
- | message | 消息。 |
七、worker 进程的启动
worker 是可执行程序,(一般来说)需要通过其他进程来启动。
启动时首先需要设置环境变量 MEDIASOUP_VERSION
,通常设置为 mediaoup 真实的版本号。当然,如果非要设置成其他内容也没关系。
1 | // File: worker/src/main.cpp |
还可以以命令行参数的方式设置 logLevel
、logTags
、rtcMinPort
和 rtcMaxPort
、dtlsCertificateFile
和 dtlsPrivateKeyFile
,分别表示日志等级、日志标签(如ice、dtls等,控制哪些类的日志才会传出)、最小和最大 rtc 端口,以及 dtls 证书文件和私钥的物理路径。这些参数都是可选的,如果未提供则会使用默认值或自动生成。
因为 worker 是独立进程,需要和父进程通信。以 Linux
为例,在 fork
子进程后会将父进程的文件描述符传递到子进程中。具体来说 Node.js 程序 fork worker 进程之前,会创建几个 libuv
概念下而非 Linux 概念下的抽象意义上的 pipe
,在 Linux 中使用的是 UNIX Domain Socket
。fork 进程后,会在子进程将要使用的文件描述符重定向。这里子进程期望持有的文件描述符是 3-6,而实际上父进程创建的可能是 11-15,fork 之后子进程得到的还是 11-15,只要在子进程中使用 fcntl
系统调用重定向即可。通过合理的数量和顺序上的约定能确定重定向为 3-6。最终在子进程中 exec mediasoup-worker(见:uv__process_child_init)。
mediasoup 抽象出叫做 ChannelSocket
和 PayloadChannelSocket
的概念来表示 worker 和父进程的通信信道。
mediasoup 最初只使用了 1 个文件描述符,后来改为了 2 个,再后来改为了 4 个。
八、以库的方式使用 worker
在较长的一段时间,worker 都是以进程的方式运行,不过直到 2021 年 3 月发布的 3.7.0
开始, 准确地说是从 1a805366
的这次提交开始,已经支持以库的方式使用。
如果要像 mediasoup-sfu-cpp 那样用纯 C++ 实现整个 mediasoup 服务端,则可以直接调用 mediasoup_worker_run
方法。当然,也就不用设置环境 MEDIASOUP_VERSION 变量了。
1 | // File: worker/src/lib.hpp |
以进程方式运行时进程间通所使用的是文件描述符(consumerChannelFd等),以库的方式运行则可以将函数指针传递给 mediasoup_worker_run 的其他参数(PayloadChannelReadFn等),都能够构造出 ChannelSocket 和 PayloadChannelSocket。
还可以用多个线程执行 mediasoup_worker_run
,以达到多个 worker 进程的效果。
备注:mediasoup-sfu-cpp 是基于旧版 mediasoup,尚未更新至可以使用 mediasoup_worker_run 方法的版本。
参考资料
现代C++和Mediasoup的WebRTC集群服务实践
纯 C++ 实现的 Mediasoup 闫华
【流媒体】Mediasoup库的架构(C++部分)
libuv
mediasoup v3 Design
mediasoup v3 API
mediasoup-client v3 API