This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
<audio id="audio" title="24 | 多人音视频实时通讯是怎样的架构?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/44/66/44a276774510df0b8dafccefd995b766.mp3"></audio>
在前面的章节里我们通过大量的篇幅介绍了WebRTC 在浏览器上对实时通信的各种支持。WebRTC 本身提供的是1对1的通信模型在 STUN/TURN 的辅助下,如果能实现 NAT 穿越,那么两个浏览器是可以直接进行媒体数据交换的;如果不能实现 NAT 穿越,那么只能通过 TURN服务器进行数据转发的方式实现通信。目前来看Google 开源的用于学习和研究的项目基本都是基于 STUN/TURN 的1对1通信。
如果你想要通过 WebRTC 实现多对多通信,该如何做呢?其实,基于 WebRTC 的多对多实时通信的开源项目也有很多,综合来看,多方通信架构无外乎以下三种方案。
- **Mesh方案**,即多个终端之间两两进行连接,形成一个网状结构。比如 A、B、C 三个终端进行多对多通信当A想要共享媒体比如音频、视频它需要分别向 B 和 C发送数据。同样的道理B 想要共享媒体,就需要分别向 A、C 发送数据,依次类推。这种方案对各终端的带宽要求比较高。
- **MCUMultipoint Conferencing Unit方案**,该方案由一个服务器和多个终端组成一个星形结构。各终端将自己要共享的音视频流发送给服务器,服务器端会将在同一个房间中的所有终端的音视频流进行混合,最终生成一个混合后的音视频流再发给各个终端,这样各终端就可以看到/听到其他终端的音视频了。实际上服务器端就是一个音视频混合器,这种方案服务器的压力会非常大。
- **SFUSelective Forwarding Unit方案**该方案也是由一个服务器和多个终端组成但与MCU不同的是SFU不对音视频进行混流收到某个终端共享的音视频流后就直接将该音视频流转发给房间内的其他终端。它实际上就是一个音视频路由转发器。
了解了上面几种通信架构后,接下来,我们分别论述一下这几种架构。
## 1对1通信
不过在讲解多对多架构模型之前,咱们得先回顾一下之前讲的 WebRTC 1对1通信的模型因为只有在1对1通信模型非常清楚的情况下你才能知道多对多通信模型的复杂度到底体现在哪儿。
在1对1通信中WebRTC 首先尝试两个终端之间是否可以通过 P2P 直接进行通信,如果无法直接通信的话,则会通过 STUN/TURN 服务器进行中转,如下图:
<img src="https://static001.geekbang.org/resource/image/e3/68/e32cd7ea6aa175c8a64739d9c95a3968.png" alt="">
上面这张图咱们已经见过无数次了相信你对它已经十分熟悉了。实际上1对1通信模型设计的主要目标是尽量让两个终端进行直联这样即可以节省服务器的资源又可以提高音视频的服务质量。
如果端与端之间可以直接通信,那么上图中的 STUN/TURN服务器的作用就只是用于各终端收集 reflx 类型的 Candidate 。而如果端与端之间无法直接进行通信的话那么STUN/TURN服务器就变成了中继服务器可以利用它进行端与端之间数据的中转。
## Mesh方案
1对1通信模型下两个终端可以互相连接那么我们是否可以让多个终端互相连接起来从而实现多人通信呢理论上这是完全可行的。Mesh 方案的结构如下图所示:
<img src="https://static001.geekbang.org/resource/image/fe/9b/fed9792cef39fbd99e4fa1e8ffdf5c9b.png" alt="">
在上图中B1、B2、B3、B4 分别表示4个浏览器它们之间两两相连同时还分别与 STUN/TURN服务器进行连接**此时的 STUN/TURN服务器不能进行数据中转否则情况会变得非常复杂**),这样就形成了一个**网格拓扑结构**。
当某个浏览器想要共享它的音视频流时它会将共享的媒体流分别发送给其他3个浏览器这样就实现了多人通信。这种结构的优势有如下
- 不需要服务器中转数据STUN/TUTN只是负责 NAT 穿越,这样利用现有 WebRTC 通信模型就可以实现,而不需要开发媒体服务器。
- 充分利用了客户端的带宽资源。
- 节省了服务器资源,由于服务器带宽往往是专线,价格昂贵,这种方案可以很好地控制成本。
当然,有优势自然也有不足之处,主要表现如下:
- 共享端共享媒体流的时候,需要给每一个参与人都转发一份媒体流,这样**对上行带宽的占用很大**。参与人越多,占用的带宽就越大。除此之外,对 CPU、Memory 等资源也是极大的考验。一般来说客户端的机器资源、带宽资源往往是有限的资源占用和参与人数是线性相关的。这样导致多人通信的规模非常有限通过实践来看这种方案在超过4个人时就会有非常大的问题。
- 另一方面,在多人通信时,如果有部分人不能实现 NAT 穿越,但还想让这些人与其他人互通,就显得很麻烦,需要做出更多的可靠性设计。
## MCU方案
MCU 主要的处理逻辑是:接收每个共享端的音视频流,经过解码、与其他解码后的音视频进行混流、重新编码,之后再将混好的音视频流发送给房间里的所有人。
MCU 技术在视频会议领域出现得非常早目前技术也非常成熟主要用在硬件视频会议领域。不过我们今天讨论的是软件MCU它与硬件MCU的模型是一致的只不过一个是通过硬件实现的另一个是通过软件实现的罢了。MCU 方案的模型是一个星形结构,如下图所示:
<img src="https://static001.geekbang.org/resource/image/a2/2a/a2667e1d47f689267168078d195c8d2a.png" alt="">
我们来假设一个条件B1 与 B2 同时共享音视频流,它们首先将流推送给 MCU 服务器MCU服务器收到两路流后分别将两路流进行解码之后将解码后的两路流进行混流然后再编码编码后的流数据再分发给 B3 和 B4。
对于 B1 来说因为它是其中的一个共享者所以MCU给它推的是没有混合它的共享流的媒体流在这个例子中就是直接推 B2 的流给它。同理对于B2来说MCU给它发的是 B1 的共享流。但如果有更多的人共享音视频流,那情况就更加复杂。
MCU 主要的处理逻辑如下图所示:
<img src="https://static001.geekbang.org/resource/image/3b/0f/3baa0690d21700210ffb0f0da6d9c10f.png" alt="">
这个处理过程如下所示:
1. 接收共享端发送的音视频流。
1. 将接收到的音视频流进行解码。
1. 对于视频流,要进行重新布局,混合处理。
1. 对于音频流,要进行混音、重采样处理。
1. 将混合后的音视频进行重新编码。
1. 发送给接收客户端。
那MCU 的优势有哪些呢?大致可总结为如下几点:
- 技术非常成熟,在硬件视频会议中应用非常广泛。
- 作为音视频网关,通过解码、再编码可以屏蔽不同编解码设备的差异化,满足更多客户的集成需求,提升用户体验和产品竞争力。
- 将多路视频混合成一路,所有参与人看到的是相同的画面,客户体验非常好。
同样MCU 也有一些不足,主要表现为:
- 重新解码、编码、混流,需要大量的运算,对 CPU 资源的消耗很大。
- 重新解码、编码、混流还会带来延迟。
- 由于机器资源耗费很大,所以 MCU 所提供的容量有限,一般十几路视频就是上限了。
## SFU方案
SFU 像是一个媒体流路由器接收终端的音视频流根据需要转发给其他终端。SFU 在音视频会议中应用非常广泛,尤其是 WebRTC 普及以后。支持 WebRTC 多方通信的媒体服务器基本都是 SFU 结构。SFU 的拓扑机构和功能模型如下图:
<img src="https://static001.geekbang.org/resource/image/74/ef/740a6afe38fba9636fc9c93e3717e0ef.png" alt="">
在上图中B1、B2、B3、B4 分别代表4个浏览器每一个浏览器都会共享一路流发给 SFUSFU 会将每一路流转发给共享者之外的3个浏览器。
下面这张图是从SFU服务器的角度展示的功能示意图
<img src="https://static001.geekbang.org/resource/image/2a/3e/2a147d9a934ba53c4548b00a0a06153e.png" alt="">
相比 MCUSFU 在结构上显得简单很多只是接收流然后转发给其他人。然而这个简单结构也给音视频传输带来了很多便利。比如SFU 可以根据终端下行网络状况做一些流控,可以根据当前带宽情况、网络延时情况,选择性地丢弃一些媒体数据,保证通信的连续性。
目前许多 SFU 实现都支持 SVC 模式和 Simulcast 模式,用于适配 WiFi、4G 等不同网络状况,以及 Phone、Pad、PC 等不同终端设备。
SFU 的优势有哪些呢?可总结为如下:
- 由于是数据包直接转发,不需要编码、解码,对 CPU 资源消耗很小。
- 直接转发也极大地降低了延迟,提高了实时性。
- 带来了很大的灵活性,能够更好地适应不同的网络状况和终端类型。
同样SFU有优势也有不足主要表现为
- 由于是数据包直接转发,参与人观看多路视频的时候可能会出现不同步;相同的视频流,不同的参与人看到的画面也可能不一致。
- 参与人同时观看多路视频,在多路视频窗口显示、渲染等会带来很多麻烦,尤其对多人实时通信进行录制,多路流也会带来很多回放的困难。总之,整体在通用性、一致性方面比较差。
通过上面的分析和比较,我相信你已经对这三种多对多音视频通信架构有了一个非常清晰的认知了。综合它们各自的优劣情况,我们可以得出 **SFU 是三种架构方案中优势最明显而劣势又相对较少的一种架构方案**。
无论是从灵活性上还是音视频的服务质量、负载情况等方面上相较其他两种方案SFU 都有明显的优势,因此这种方案也被大多数厂商广泛采用。
另外,在上面介绍 SFU 方案时,我们还提到了视频的 **Simulcast** 模式和 **SVC** 模式,下面我就这两个知识点再向你做一下讲解,来看一下这两种视频的处理模式对 SFU 架构来说都带来了哪些好处。
### 1. Simulcast 模式
所谓**Simulcast模式就是指视频的共享者可以同时向 SFU 发送多路不同分辨率的视频流**(一般为三路,如 1080P、720P、360P。而 SFU 可以将接收到的三路流根据各终端的情况而选择其中某一路发送出去。例如,由于 PC 端网络特别好,给 PC 端发送 1080P 分辨率的视频;而移动网络较差,就给 Phone 发送 360P 分辨率的视频。
Simulcast 模式对移动端的终端类型非常有用,它可以灵活而又智能地适应不同的网络环境。下图就是 Simulcast 模式的示意图:
<img src="https://static001.geekbang.org/resource/image/07/30/079b88b254bb11887c093464b5737630.png" alt="">
### 2. SVC模式
SVC 是可伸缩的视频编码模式。与 Simulcast 模式的同时传多路流不同SVC 模式是在视频编码时做“手脚”。
它在视频编码时将视频分成多层——核心层、中间层和扩展层。**上层依赖于底层,而且越上层越清晰,越底层越模糊**。在带宽不好的情况下,可以只传输底层,即核心层,在带宽充足的情况下,可以将三层全部传输过去。
如下图所示PC1 共享的是一路视频流,编码使用 SVC 分为三层发送给 SFU。SFU 根据接收端的情况发现PC2网络状况不错于是将 0、1、2 三层都发给 PC2发现 Phone 网络不好,则只将 0 层发给 Phone。这样就可以适应不同的网络环境和终端类型了。
<img src="https://static001.geekbang.org/resource/image/ef/02/ef4e0aa7d5053e822ada4b6e8987a502.png" alt="">
## 小结
本文我向你介绍了三种多方通信的架构,分别是 Mesh、MCU和SFU。
整体来看由于各方面限制Mesh 架构在真实的应用场景中几乎没有人使用,一般刚学习 WebRTC 的同学才会考虑使用这种架构来实现多方通信。
MCU 架构是非常成熟的技术,在硬件视频会议中应用非常广泛。像很多做音视频会议的公司之前都会购买一套 MCU 设备,这样一套设备价格不菲,最低都要 50万但随着互联网的发展以及音视频技术越来越成熟硬件MCU已经逐步被淘汰。
当然现在也还有公司在使用软MCU比较有名的项目是FreeSWITCH但正如我们前面所分析的那样由于MCU要进行解码、混流、重新编码的操作这些操作对CPU消耗是巨大的。如果用硬件MCU还好但软MCU这个劣势就很明显了所以真正使用软MCU架构方案的公司也不多。
SFU是最近几年流行的新架构目前 WebRTC 多方通信媒体服务器都是 SFU 架构。从上面的介绍中你也可以了解到 SFU 这种架构非常灵活,性能也非常高,再配上视频的 Simulcast 模式或 SVC 模式,则使它更加如虎添翼,因此各个公司目前基本上都使用该方案。
## 思考时间
今天我留给你的思考题是:在 SFU 方案中,该如何将多路音视频流录制下来并进行回放呢?
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,147 @@
<audio id="audio" title="25 | 那些常见的流媒体服务器,你该选择谁?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/03/be/03aab2fd4fb54d81fd590867e57be5be.mp3"></audio>
在[上一篇文章](https://time.geekbang.org/column/article/132863)中我们详细讨论了三种类型Mesh、MCU和SFU的多方通信架构并从中知道**SFU方案是目前最优的一种多方通信架构方案**,而且这种方案目前已经有很多开源的实现了,比较流行的开源项目包括 Licode、Janus-gateway、MediaSoup、Medooze等。
当然你也可以自己实现SFU流媒体服务器但自已实现流媒体服务器困难还是蛮多的它里面至少要涉及到 DTLS 协议、ICE协议、SRTP/SRTCP协议等光理解这些协议就要花不少的时间更何况要实现它了。
而使用开源的流媒体服务器就可以大大地减少你的工作量,但这也带来一个问题:这么多开源流媒体服务器,究竟该选哪一个呢?这就涉及到一个选型的问题,你必须要对上面提到的开源项目有个清楚的认知,知道它们各自的优劣点,才能选出更适合的那款流媒体服务器。
接下来我们就按顺序把这几个开源实现向你做下介绍,从而让你了解它们的优缺点,以便选出更适合你的那款流媒体服务器。
## Licode
Licode 既可以用作 SFU 类型的流媒体服务器,也可以用作 MCU 类型的流媒体服务器。一般情况下,它都被用于 SFU 类型的流媒体服务器。
Licode 不仅仅是一个流媒体通信服务器,而且还是一个包括了媒体通信层、业务层、用户管理等功能的完整系统,并且该系统还支持分布式部署。
Licode 是由 C++ 和 Node.js 语言实现。其中,媒体通信部分由 C++ 语言实现,而信令控制、用户管理、房间管理用 Node.js 实现。它的源码地址为:[https://github.com/lynckia/licode](https://github.com/lynckia/licode) 。下面这张图是 Licode 的整体架构图:
<img src="https://static001.geekbang.org/resource/image/80/45/80c24c5862342103b1b2dcb134b59d45.png" alt="">
通过这张图你可以看出Licode从功能层面来讲分成三部分即 Nuve 、ErizoController和ErizoAgent 三部分,它们之间通过消息队列进行通信。
- Nuve 是一个 Web 服务,用于管理用户、房间、产生 token以及房间的均衡负载等相关工作。它使用 MongoDB 存储房间和token信息但不存储用户信息。
- ErizoController用于管理控制信令和非音视频数据都通过它接收。它通过消息队列与 Nuve进行通信也就是说 Nuve 可以通过消息队列对 ErizoController 进行控制。
- ErizoAgent用于音视频流媒体数据的传输可以分布式布署。ErizoAgent 与 ErizoController的通信也是通过消息队列信令消息通过 ErizoController 接收到后,再通过消息队列发给 ErizoAgent从而实现对 ErizoAgent 进行控制。
通过上面的描述,你可以知道 Licode 不仅仅是一个SFU流媒体服务器它还包括了与流媒体相关的业务管理系统、信令系统、流媒体服务器以及客户端 SDK等等可以说它是一个比较完善的产品。
如果你使用 Licode 作为流媒体服务器,基本上不需要做二次开发了,所以这样一套系统对于没有音视频积累的公司和个人具有非常大的诱惑力。目前 Intel CS 项目就是在 Licode 基础上研发出来的,已经为不少公司提供了服务。
但Licode也有以下一些缺点
- 在Linux下目前只支持 Ubuntu 14.04版本,在其他版本上很难编译通过。
- Licode 不仅包括了 SFU而且包括了 MCU所以它的代码结构比较重学习和掌握它要花不少的时间。
- Licode的性能一般 如果你把流媒体服务器的性能排在第一位的话那么Licode就不是特别理想的 SFU 流媒体服务器了。
## Janus-gateway
Janus 是一个非常有名的WebRTC流媒体服务器它是以 Linux 风格编写的服务程序,采用 C 语言实现,支持 Linux/MacOS 下编译、部署,但不支持 Windows 环境。
它是一个开源项目其源码的编译、安装非常简单只要按GitHub上的说明操作即可。源码及编译手册的地址为[https://github.com/meetecho/janus-gateway](https://github.com/meetecho/janus-gateway) 。
Janus 的部署也十分简单,具体步骤详见文档,地址为:[https://janus.conf.meetecho.com/docs/deploy.html](https://janus.conf.meetecho.com/docs/deploy.html) 。
下面我们来看一下 Janus 的整体架构,其架构如下图所示:
<img src="https://static001.geekbang.org/resource/image/d9/f5/d9009bfef6537f9b2a803dc6ca80a3f5.png" alt="">
从上面的架构图中,你可以看出 Janus 分为两层,即**应用层**和**传输层**。
**插件层又称为应用层**,每个应用都是一个插件,可以根据用户的需要动态地加载或卸载掉某个应用。插件式架构方案是非常棒的一种设计方案,灵活、易扩展、容错性强,尤其适用于业务比较复杂的业务,但缺点是实现复杂,成本比较高。
在 Janus 中默认支持的插件包括以下几个。
- **SIP**:这个插件使得 Janus 成了SIP用户的代理从而允许WebRTC终端在SIP服务器如Asterisk上注册并向SIP服务器发送或接收音视频流。
- **TextRoom**:该插件使用 DataChannel实现了一个文本聊天室应用。
- **Streaming**它允许WebRTC终端观看/收听由其他工具生成的预先录制的文件或媒体。
- **VideoRoom**:它实现了视频会议的 SFU 服务,实际就是一个音/视频路由器。
- **VideoCall**:这是一个简单的视频呼叫的应用,允许两个 WebRTC 终端相互通信,它与 WebRTC官网的例子相似[https://apprtc.appspot.com](https://apprtc.appspot.com)),不同点是这个插件要经过服务端进行音视频流中转,而 WebRTC 官网的例子走的是P2P直连。
- **RecordPlay**该插件有两个功能一是将发送给WebRTC的数据录制下来二是可以通过WebRTC进行回放。
传输层包括**媒体数据传输**和**信令传输**。**媒体数据传输层**主要实现了 WebRTC 中需要有流媒体协议及其相关协议如DTLS协议、ICE协议、SDP协议、RTP协议、SRTP协议、SCTP协议等。
**信令传输层**用于处理 Janus 的各种信令,它支持的传输协议包括 HTTP/HTTPS、WebSocket/WebSockets、NanoMsg、MQTT、PfUnix、RabbitMQ。不过需要注意的是有些协议是可以通过编译选项来控制是否安装的也就是说这些协议并不是默认全部安装的。另外Janus所有信令的格式都是采用 Json 格式。
通过上面的分析你可以知道Janus 整体架构采用了插件的方案,这种架构方案非常优秀,用户可以根据自己的需要非常方便地在上面编写自己的应用程序。而且它目前支持的功能非常多,比如支持 SIP、 RTSP、音视频文件播放、录制等等所以在与其他系统的融合性上有非常大的优势。另外它底层的代码是由 C 语言编写的性能也非常强劲。Janus的开发、部署手册也非常完善因此它是一个非常棒的开源项目。
如果说Janus 有什么缺点的话,那就是它的架构设计比较复杂,对于初学者来说难度较大。
## Mediasoup
Mediasoup 是推出时间不长的 WebRTC 流媒体服务器开源库,其地址为:[https://github.com/versatica/mediasoup/](https://github.com/versatica/mediasoup/) 。
Mediasoup 由**应用层**和**数据处理层**组成。应用层是通过Node.js 实现的;数据处理层由 C++ 语言实现包括DTLS协议实现、ICE协议实现、SRTP/SRTCP协议实现、路由转发等。
下面我们来看一下 Mediasoup 的架构图,如下所示:
<img src="https://static001.geekbang.org/resource/image/4c/a0/4cb7be0e4c7c40d9d72b635fb54ce4a0.png" alt="">
Mediasoup 把每个实例称为一个 Worker在 Worker 内部有多个 Router每个 Router 相当于一个房间。在每个房间里可以有多个用户或称为参与人每个参与人在Mediasoup中由一个 Transport代理。换句话说对于房间Router来说Transport 就相当于一个用户。
Transport有三种类型即 WebRtcTransport、PlainRtpTransport和PipeTransport。
- WebRtcTransport 用于与 WebRTC 类型的客户端进行连接,如浏览器。
- PlainRtpTransport 用于与传统的 RTP 类型的客户端连接,通过该 Transport 可以播放多媒体文件、FFmpeg的推流等。
- PipeTransport 用于 Router 之间的连接也就是一个房间中的音视频流通过PipeTransport传到另一个房间。
在每个 Transport 中可以包括多个 Producer 和 Consumer。
- Producer 表示媒体流的共享者,它又分为两种类型,即音频的共享者和视频的共享者。
- Consumer 表示媒体流的消费者,它也分为两种类型,即音频的消费者和视频的消费者。
Mediasoup的实现逻辑非常清晰它不关心上层应用该如何做只关心底层数据的传输并将它做到极致。
Mediasoup 底层使用 C++ 开发,使用 libuv 作为其异步IO事件处理库所以保证了其性能的高效性。同时它支持了几乎所有 WebRTC 为了实时传输做的各种优化,所以说它是一个特别优秀的 WebRTC SFU 流媒体服务器。
它与 Janus 相比,它更聚焦于数据传输的实时性、高效性、简洁性,而 Janus 相比 Mediasoup 做的事儿更多,架构和逻辑也更加复杂。所以**对于想学习WebRTC 流媒体服务器源码的同学来说Mediasoup是一个非常不错的项目**。
另外,对于开发能力比较强的公司来说,根据自己的业务需要在 Mediasoup 上做二次开发也是非常值得推荐的技术方案。
## Medooze
Medooze 是一款综合流媒体服务器,它不仅支持 WebRTC 协议栈,还支持很多其他协议,如 RTP、RTMP等。其源码地址为[https://github.com/medooze/media-server](https://github.com/medooze/media-server) 。
下面我们来看一下 Medooze 的架构图:
<img src="https://static001.geekbang.org/resource/image/9b/9f/9baeed8af646af53c716c1e97e600a9f.png" alt="">
从大的方面来讲Medooze 支持 RTP/RTCP、SRTP/SRCP 等相关协议,从而可以实现与 WebRTC终端进行互联。除此之外Medooze还可以接入RTP流、RTMP流等因此你可以使用GStreamer/FFmpeg向Medooze推流这样进入到同一个房间的其他 WebRTC 终端就可以看到/听到由 GStream/FFmpeg 推送上来的音视频流了。另外Medooze 还支持录制功能,即上图中的 Recorder 模块的作用,可以通过它将房间内的音视频流录制下来,以便后期回放。
为了提高多方通信的质量Medooze 在音视频的内容上以及网络传输的质量上都做了大量优化。关于这些细节我们这里就不展开了,因为在后面的文章中我们还会对 Medooze 作进一步的讲解。
以上我们介绍的是 Medooze的核心层下面我们再来看看Medooze的控制逻辑层。Medooze 的控制逻辑层是通过 Node.js 实现的Medooze 通过 Node.js 对外提供了完整的控制逻辑操作相关的 API通过这些 API 你可以很容易的控制 Medooze 的行为了。
通过上面的介绍,我们可以知道 Medooze 与 Mediasoup 相比两者在核心层实现的功能都差不多但Medooze的功能更强大包括了录制、推RTMP流、播放 FLV 文件等相关的操作而Mediasoup则没有这些功能。
不过 Medooze 也有一些缺点,尽管 Medooze 也是 C++ 开发的流媒体服务务器,使用了异步 IO 事件处理机制,但它使用的异步 IO 事件处理的 API 是 pollpoll 在处理异步 IO 事件时与Linux下最强劲的异步IO事件API epoll 相比要逊色不少,这导致它在接收/发送音视频包时性能比Mediasoup要稍差一些。
## 如何选择 SFU
上面我们对Licode、Janus、Mediasoup、Medooze 这几个开源的 WebRTC 流媒体服务器做了比较,从中你应该对它们的架构和特性有了一个基本了解。整体上看这些流媒体服务器各有优缺点,对于我们来说到底该选择那个项目作为我们的流媒体服务器呢?
其实,对流媒体服务器的选择是一个 Balance没有最好只有最合适。每个开源实现都有其各自的特点都可以应用到实际产品中只不过作为开发人员都有自己独特的技术背景**你需要根据自身特点以及项目特点选一个最合适的**。接下来,我就介绍一下我是如何对这些开源项目进行评判和选择的。
在选择之前我会像上面一样对要选择的项目有个大体的了解,然后通过以下几个方面来对项目进行打分,从而选出最适合我的项目。
**首先是实现语言**。在一个团队中肯定会选择一种大家都比较熟悉的语言作为项目开发的语言,所以我们在选择开源项目时,就要选择使用这种语言开发的开源项目。比如阿里系基本都用 Java 语言进行开发,所以它们在选择开源项目时,基本都会选择 Java开发的开源项目而做音视频流媒体服务的开发人员为了追求性能所以一般都选择 C/C++ 语言开发的开源项目。
Meooze、Mediasoup、Licode 这三个流媒体服务器的媒体通信部分都是由 C++ 实现的,而控制逻辑是通过 Node.js 实现因此如果你是C++开发人员,且有 JavaScript 技术背景那么你就应该在这三种流媒体服务器之间选择因为这样更容易入门。而Janus-gateway 是完全通过 C 语言实现的,服务部署是传统的 Linux 风格,因此如果你是 Linux/C 开发者,则应该选择 Janus 作为你的流媒体服务器。
**其次是系统特点**。像Licode 是一个完整的系统支持分布式集群部署所以系统相对复杂学习周期要长一些。它可以直接布署在生产环境但是二次开发的灵活性不够。Janus-gateway 是一个独立的服务,支持的信令协议很丰富,而且支持插件开发,易扩展,对于 Linux/C 背景的开发者是很不错的选择。Medooze 和 Mediasoup 都是流媒体服务器库,对于需要将流媒体服务器集成到自己产品中的开发者来说,应该选择它们。
另外从性能方面讲Licode、Meooze、Mediasoup、Janus-gateway 单台服务都可以支持500 方参会人所以它们的性能都还是不错的。相对来说Licode的性能与其他流媒体服务器相比要低一些Medooze 由于没有使用epoll来处理异步IO事件所以性能也受到一些影响。不过总的来说它们在500方的容量下视频质量都可以得到很好的保证延迟在 100ms左右。
## 小结
本文我向你介绍了 Licode、Meooze、Mediasoup、Janus-gateway 四个开源 SFU 的架构及其特点。
从 SFU 模型来看,它们的实现基本都类似的,只是概念叫法不同,它们之间的差异主要体现在系统对外提供的接口上。对于想要研究 SFU 基本原理的开发者来说,只要选择其中一个项目仔细研读即可。但我个人还是建议初学者选择 Mediasoup 作为研究SFU原理的对象因为它结构简单代码量少很容易入门。当然如果你是一个纯 C 技术背景的开发者,可以选择 Janus-gateway作为研究对象。
如果说还需要研究录制回放、MCU 的开发者,可以去研究 Medooze。Medooze 结构比较完整,功能齐全,代码量也不大。而如果你想要让自己的媒体服务器支持集群部署,可以参考 Licode。
## 思考时间
如果让你自己实现一个 SFU 流媒体服务器你会怎么设计你认为都有哪些问题是实现SFU的关键呢
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,244 @@
<audio id="audio" title="26 | 为什么编译Medooze Server这么难" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a7/c9/a7194b49043bc35841b08a117b88e1c9.mp3"></audio>
在[上一篇文章](https://time.geekbang.org/column/article/134284)中,我们对 Licode、Janus、Mediasoup以及Medooze 四个 WebRTC 开源流媒体服务器的实现进行对比对于想研究音视频多方会议、录制回放、直播等流媒体技术的开发人员来说Medooze 是很好的选择。因为它支持所有这些功能,通过它的源码及其 Demo 就可以对 Medooze 进行深入学习了。
从本文开始,在接下来的四篇文章我会向你详细讲述 Medooze 的架构、实现以及应用。而本文主要介绍Medooze是如何编译和构建的。
也许你会觉得Linux下的程序编译有什么可讲的呢直接在Linux系统下执行一下 build 命令不就行了,还需要专门理解系统的编译过程和构建工具的使用吗?实际上,根据我多年的工作经验来看,理解项目的编译过程、熟悉各种构建工具的使用是非常有必要的。下面我就举几个例子,通过这几个例子,我想你就会对它们有一个深刻感悟了。
第一个例子,伴随着技术的飞速发展,构建工具也有了很大的变化,从最早的手动编写 Makefile逐渐过渡到使用 Autotools、CMake、GYP 等构建工具来生成 Makefile这些构建工具可以大大提高你的工作效率。比如通过Andorid Studio 创建JNI程序时在 Android Studio 底层会使用 CMake 来构建项目,你会发现使用 CMake 构建 JNI 非常方便。然而像Chrome浏览器这种大型项目也用 CMake构建就很不合适了因为Chrome浏览器的代码量巨大使用 CMake 构建它会花费特别长的时间(好几个小时),为了解决这个问题 Chrome 团队自己开发了一个新的构建工具即GYP来构建这种大型项目从而大大提高了构建的效率。由此可见不同的项目使用不同的构建工具对开发效率的提升是巨大的。
第二个例子,当你研究某个开源项目的源代码时,如果你对 Makefile语法非常熟悉的话它对你研读项目的源码会起到事半功倍的效果。因为通过 Makefile 你可以了解到项目中引入了哪些第三方库、源文件编译的先后顺序是怎样的、都定义了那些宏等信息,这些信息都可以帮助你更好地理解源码中的实现逻辑。
第三个例子对于某些开源项目很多时候其构建文档并不完整因此在构建过程中经常会出现各种失败而这种失败会让人不知所措。但如果你对Makefile特别熟悉的话你就可以去阅读它里面的内容很多时候通过这种方法就可以很快找到编译失败的真正原因。
通过上面的描述,我相信你应该已经知道理解项目的构建过程,熟悉各种构建工具的使用对开发人员有多重要了。
那接下来我们言归正传来看看该如何编译Media-server-node 。
## Media-server-node 项目
Media-server-node 是一个 Node.js 项目,其目录结构如下图所示:
<img src="https://static001.geekbang.org/resource/image/1b/2e/1b7e5aa7de762d7bd1606c0dbc14382e.png" alt="">
通过上图你可以看到,在 Media-server-node 项目中,用红框框出来的四个目录比较重要。其中**src 目录**中存放的是 C++ 语言实现的 Native 代码,用于生成 JavaScript 脚本可以调用的 C++ 接口,从而使 JavaScript 编写的业务层代码可以调用 Medooze 核心层的 C++代码;**lib 目录**里存放的是在 Node.js 端执行的,控制 Medooze 业务逻辑的 JavaScript 代码;**external 目录**中存放的是 Medooze 使用的第三方库,如 MP4v2 和 SRTP**media-server 目录**是一个比较重要的目录,它里面存放的是 **Medooze 的核心代码,即 C++ 实现的流媒体服务器代码**
这里需要注意的是Media-server-node 中的 media-server 目录是一个独立的项目它是通过外部链接引用到Media-server-node项目中的。它既可以随 Media-server-node 一起编译,也可以自己单独编译。对于这一点我们后面还会做详细介绍。
### 1. 构建Media-server-node项目
构建 Media-server-node 项目非常简单,只需要执行下面的命令即可构建成功:
```
npm install medooze-media-server --save
```
实际上,在构建 Media-server-node 项目时,重点是对上面三个 C/C++ 目录(即 external、media-server、src中的 Native 代码的构建。项目构建时会调用 node-gyp 命令将三个目录中的 C++ 代码编译成 Node.js 的 Native 插件,以供 JavaScript 脚本使用。
>
另外Media-server-node 目录中的 binding.gyp 文件就是供node-gyp使用来构建C++ Navtie 代码的规则文件。在执行上面的构建命令时,底层会调用 node-gyp命令node-gyp 以binding.gyp为输入然后根据 binding.gyp 中的规则对C++ Native代码进行构建的。
下面我们就来看看node-gyp 是如构建 C++ Native代码的吧。
### 2. node-gyp &amp; GYP
首先我们来了解一下node-gyp**node-gyp 是一个由 Node.js 执行的脚本程序,是专门用于生成在 Node.js 中运行的 C/C++ Native 插件的工具**。
实际上,你可以简单地将 node-gyp 认为是 gyp 和 Make/Ninja 工具的集合当构建某个项目时在底层node-gyp先使用 gyp 工具根据 binding.gyp 中的规则生成Makefile或 build.ninja文件然后再调用 Make/Ninja 命令进行构建,编译出 Native 插件。
那上面说的GYP又是什么呢GYPGenerate Your Projects是 Chromium 团队使用 Python 语言开发的构建工具,它以 .gyp 为输入文件,可以产生不同平台的 IDE 工程文件如VS 工程;或产生各种编译文件,如 Makefile 。
通过上面的描述你可以知道,存在着两个层面的规则文件,.gyp 是由 GYP工具使用的项目规则文件Makefile/build.ninja是由 Make/Ninja 使用的编译规则文件我们称它为编译文件。另外Make/Ninja 命令相对更底层一些,它们执行时会直接调用编译器(如 GCC/G++)对源码进行编译。而 gyp命令更偏项目管理一些它是为了产生各种工程或Makefile/build.ninja文件而存在的。
有很多同学对 node-gyp、GYP、Make、Ninja 这些工具是什么关系分不清,通过上面的讲解你应该就清楚它们之间的区别了。下面这张图将它们之间的关系描述得更加清晰:
<img src="https://static001.geekbang.org/resource/image/6a/01/6ab82535c85296cdaba1da662045b701.png" alt="">
通过上图我们可以看到 gyp 命令是将 binding.gyp文件生成Makefile文件然后交给 make最终将 Native 插件编译出来的。
了解了 node-gyp 和 GYP 之间的关系之后,我们再来了解一下 GYP 规则的语法。首先我们要知道 GYP 规则文件是以 JSON 格式存储的。另外,在 GYP 的规则文件中,它按有效范围将规则分为了两大类,即全局规则和局部规则。下面我们就以 binding.gyp 为例,看看它是如何使用这些规则的。
所谓全局规则就是指这些规则在整个文件内有效的规则,我们看一下代码吧:
<img src="https://static001.geekbang.org/resource/image/22/7f/222c0e4c78f751ddc9293db790e8e57f.png" alt="">
代码中的 **variables** 用于在GYP规则文件中定义全局变量这些全局变量被定义好后就可以在规则文件中的任何地方被引用了。
GYP中除了**variables**外,还定义了其他几个全局规则,具体的内容可以查看文末的参考一节,这里就不一一列出了。
在规则文件中最重要的规则要数 target了它属于局部规则在 binding.gyp 文件中的 target 描述如下所示:
```
&quot;targets&quot;:
[
{
&quot;target_name&quot;: &quot;medooze-media-server&quot;,
&quot;type&quot;: &quot;static_library&quot;,
&quot;cflags&quot;: //设置编译器编译参数
[
...
&quot;-O3&quot;,
&quot;-g&quot;,
...
],
&quot;ldflags&quot; : [&quot; -lpthread -lresolv&quot;], //设置编译器连接参数
&quot;include_dirs&quot; : //项目需要的头文件目录
[
'/usr/include/nodejs/',
&quot;&lt;!(node -e \&quot;require('nan')\&quot;)&quot;
],
&quot;link_settings&quot;:
{
'libraries': [&quot;-lpthread -lpthread -lresolv&quot;] //链接时使用的第三方库
},
&quot;sources&quot;: //所有需要编译的源码文件
[
&quot;src/media-server_wrap.cxx&quot;,
...
],
&quot;dependencies&quot;:[ //指定依赖的其他的 target
...
.
],
&quot;conditions&quot; : [ //编译条件
[&quot;target_arch=='ia32'&quot;, {
...
}],
...
['OS==&quot;linux&quot;',{
...
}],
],
}
]
```
下面我就向你详细讲解一下 target 中每个规则的作用和含义。
**target_name** 是target 的名字,在一个 .gyp 文件中,名字必须是唯一的,这里是 medooze-media-server。当使用 GYP 生成工程文件时,如 VS 工程或XCode工程target_name 就是各工程文件的名字。
**type**,指明了生成的 target 的类型你可以设置为以下几种类型executable 表示要生成可执行程序,在 Windows 中就是 exe 文件static_library 表示要生成静态库,生成的文件为 `*.a` 或者是`*.lib` 后辍的文件shared_library 表示要生成共享库,也就是文件后辍名为.so 或者 .dll 的文件none表示为无类型该类型留作特殊用途使用。
……
由于篇幅的原因,其他规则就不在这里一一列举了,如果你对它们感兴趣的话可以查看后面的参考一节。
通过上面的描述可以知道,在调用 npm 构建 Media-server-node 项目时,在它的内部会调用 node-gyp 命令,该命令以 binding.gyp 为输入文件,按照该文件中的规则生成 Makefile 或 build.ninja 文件,最后使用 Make/Ninja 命令来编译 C/C++ 的 Native 代码。这就是 node-gyp 执行编译的基本过程。
## 单独构建media-server项目
media-server 是 Medooze 流媒体服务器部分的实现,它用 C++ 实现。由于采用了 C++17 语法,所以需要使用较高版本 GCC 编译器。接下来我们就来看看该如何单独构建 Medooze 的 media-server 项目。
>
我的构建环境如下,操作系统 Ubuntu18.04 编译器版本GCC 7.3.0 。
### 1. 安装依赖库
由于 Medooze 不仅支持 SFU而且还支持 MCU 功能,所以它依赖音视频的编解码库和 FFmpeg 库。除此之外,为了与浏览器对接,它还依赖 libssl 库。因此,在构建 media-server 之前我们需要先将这些依赖库安装好。
安装依赖库的命令如下:
```
sudo apt-get install libxmlrpc-c++8-dev
sudo apt-get install libgsm1-dev
sudo apt-get install libspeex-dev
sudo apt-get install libopus-dev
sudo apt-get install libavresample-dev
sudo apt-get install libx264-dev
sudo apt-get install libvpx-dev
sudo apt-get install libswscale-dev
sudo apt-get install libavformat-dev
sudo apt-get install libmp4v2-dev
sudo apt-get install libgcrypt11-dev
sudo apt-get install libssl1.0-dev
```
安装好上面的依赖库后,我们就可以从 GitHub上获取 media-server 项目的代码进行编译了。media-server 项目的源代码地址为:[https://github.com/medooze/media-server.git](https://github.com/medooze/media-server.git) 。
>
<p>需要注意是的,获取代码时,你需要切换到 optimizations 分支。我最开始使用的是 master 分支但在这个分支上会遇到问题切换到optimizations<br>
分支后就没有任何问题了。</p>
media-server代码下载好后进入到该项目的目录中然后修改它的配置文件config.mk修改的内容如下所示
```
...
SRCDIR = /home/xxx/media-server //注意media-server的完整路径后面不能有空格
...
```
通过上面的修改,我们就将编译 media-server 的准备工作完成了,接下来的事情就比较简单了。
### 2. 编译common_audio
在开始编译 media-server之前我们还要先将 common_audio 模块编译出来因为common_audio 是一个公共模块media-server 依赖于它。
common_audio 模块是通过 Ninja 编译的,因此我们还需要先安装 ninja-build工具等ninja-build工具安装好后执行下面的命令就可以将common_audio 编译好了。
```
sudo apt-get install ninja-build
cd media-server/ext/out/Release
ninja
```
### 3. 编译 media-server
有了 common_audio 模块之后,编译 media-server就简单多了只需要执行下面的命令行即可
```
cd media-server
make
```
至此media-server 就编译好了。需要注意的是,目前 medooze 的 master 分支代码提交比较频繁,经常会出现编译失败的问题,所以建议你选择稳定的分支进行编译。
另外,如果你在编译过程中遇到错误,可以按照编译的提示信息进行处理,一般都能将问题解决掉。如果是依赖库缺失的问题,则还需要像上面一样用 `apt install` 命令将需要的库安装好再重新对media-server进行编译。
### 4. Ninja
在上面的描述中我们看到了 Ninja 命令它又是干什么的呢Ninja 与 Make 工具处于同一级别,它们都是从规则文件中读取内容,然后按照规则调用编译工具如 GCC/G++、CLANG/CLANG++ 编译源码文件,只不过 Make 的编译文件是 Makefile而 Ninja 的编译文件是 build.ninja。
Ninja 是在 Chrome 浏览器的研发过程中开发出来的,与 GYP一起被研发出来。最早 Chrome 也选择 Make 作为构建工具,然而随着 Chrome 浏览器源码的增长,通过 Make 构建Chrome的时间越来越长尤其在项目中有超过 30000 个源文件的大型项目中Make 工具构建项目的时长已经无法让人忍受了。因此 Chrome 工程师开发了一个新的构建工具,即 Ninja。Ninja 的设计哲学是简单、快,因此,它的出现大地的缩短了大型项目的构建时间。
Ninja 规则文件的文件名以 .ninja 结尾,所以如果我们在项目中看到 .ninja 的文件就可以判断出该项目的构建需要使用 Ninja 工具。Ninja 规则文件一般不需要手写,而是通过生产工具自动生成,像 GYP、CMake 都可以生成 Ninja 规则文件。
Ninja 的使用非常简单,只要将 Ninja 安装好后,进入到项目目录中,然后运行 Ninja 命令即可。Ninja 运行时默认会在当前目录中查找 build.ninja 文件,如果找到该文件的话,它就按照 build.ninja 中的规则来编译所有的已修改的文件。当然你也可以通过参数指定某个具体的 Ninja 规则文件,如:
```
ninja -f xxxxx.ninja
```
这样,它就可以按 xxxxx.ninja 文件中的规则对源码文件进行编译。
## 小结
本文我们以 Media-server-node 为例,首先向你介绍了如何通过 node-gyp 命令构建 Node.js 的Native 插件。通过该讲解,你应该对 Node.js 有了更深的认知,它不光可以让你用 JavaScript 脚本编写服务器程序,而且还可以编写 C/C++的Native插件来增强Node.js的功能。
另外,我们还介绍了 GYP 规则文件,并以 media-server-node 项目的构建为例,详细讲解了 GYP 文件的一些重要规则及其作用。
通过本文的学习,我相信你已经对 node-gyp、GYP、Make、Ninja 等工具有了比较清楚的了解这不仅可以帮你解决Medooze编译经常失败的问题而且还会对你分析 Medooze 源码起到非常大的帮助作用因为代码中很多宏的定义以及一些逻辑分支的走向都需要通过Medooze的编译过程进行追踪。所以学习好本文对后面的学习有着至关重要的作用。
## 思考时间
知道了 node-gyp、GYP、Make/Ninja 的关系,那你能说说 CMake 在项目编译中的作用以及与 make 的关系吗?
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。
## 参考
GYP规则含义表<br>
<img src="https://static001.geekbang.org/resource/image/56/7a/56359e4a988c2cfe2acaced69b974b7a.png" alt="">

View File

@@ -0,0 +1,174 @@
<audio id="audio" title="27 | 让我们一起探索Medooze的具体实现吧" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5e/9e/5e8fabb16883ecef634008fe3b37db9e.mp3"></audio>
在咱们专栏的第一模块,我向你介绍了如何使用 WebRTC 进行实现音视频互动。随着 Google 对 WebRTC 的大力推广,目前主流的浏览器都支持了 WebRTC。WebRTC 最主要的功能就是提供端对端的音视频通信,其可以借助 STUN/TURN 服务器完成 NAT 穿越,实现两个端点之间的直接通信。
1对1的实时通信无疑是 WebRTC 最主要的应用场景,但你是否想过 WebRTC 可以通过浏览器实现多人音视频会议呢?更进一步,你是否想过 WebRTC 可以实现一些直播平台中上万人甚至几十万人同时在线的场景呢?
如果你对上面的问题感兴趣,那本文将让你对上面问题有个全面的了解,接下来就让我们一起开启本次神秘之旅吧!
## 流媒体服务器Medooze
正如我们在[《25 | 那些常见的流媒体服务器,你该选择谁?》](https://time.geekbang.org/column/article/134284)一文中介绍的,要实现多个浏览器之间的实时音视频通信(比如音视频会议),那么就一定需要一个支持 WebRTC 协议的流媒体服务器。目前,有很多开源的项目支持 WebRTC 协议, Medooze 就是其中之一。
Medooze的功能十分强大通过它你既可以实现 SFU 类型的流媒体服务器也可以实现MCU类型的流媒体服务器。而且它还支持多种媒体流接入也就是说你除了可以通过浏览器WebRTC向Medooze分享音视频流之外还可以使用 FFmpeg/VLC 等工具将 RTMP 协议的音视频流推送给 Meoodze。更加神奇的是无论是 WebRTC 分享的音视频流还是 RTMP 格式的音视频流通过Medooze转发后浏览器就可以将 FFmpeg/VLC推的流显示出来而VLC也可以将 WebRTC 分享的音视频显示出来,这就是 Medooze 的强大之处。
接下来我们将从Medooze的 SFU 模型、录制回放模型和推流模型这三个方面向你全面、详细地介绍 Medooze。
### 1. Medooze的 SFU 模型
SFU 模型的概念我们已经在[《24 | 多人音视频实时通讯是怎样的架构?》](https://time.geekbang.org/column/article/132863)一文中做过介绍了,这里就不再赘述了!下面这张图是 Medooze 实现 SFU 服务器的大体结构图:
<img src="https://static001.geekbang.org/resource/image/f9/94/f9d36818221629f67f06a49eb4003294.png" alt="">
图的最外层标有 Browser 的模块表示浏览器,在本图中就是表示基于浏览器的 WebRTC 终端。
图的中间标有 Medooze 的模块是服务器,实现了 SFU 功能。其中 DTLSICETransport 既是传输层控制socket收发包也是一个容器在该容器中可以放IncomingStream 和 OutgoingStream。
- IncomingStream代表某客户端共享的音视频流。
- OutgoingStream代表某个客户端需要观看的音视频流。
你也可以把它们直接当作是两个队列IncomingStream 是输入队列OutgoingStream是输出队列。
有了上面这些概念那我们来看看在Medooze中从一端进来的音视频流是怎么中转给其他人的。
- 首先Browser 1 推送音视频流给 MedoozeMedooze 通过 DTLSICETransport 接收音视频数据包,然后将它们输送到 IncomingStream 中。
- 然后Medooze 再将此流分发给 Browser 2、Browser 3、Browser 4 的 OutgoingStream。
- 最后Medooze 将 OutgoingStream 中的音视频流通过 DTLSICETransport 传输给浏览器客户端,这样 Browser 2、Browser 3、Browser 4 就都能看到 Browser 1 共享的音视频流了。
同理Browser 3推送的音视频流也是按照上面的步骤操作最终分发到其他端其他端也就看/听到 Browser 3 的音视频了。
通过以上步骤就实现了多人之间的音视频互动。
### 2. 录制回放模型
下图是Medooze的录制回放模型
<img src="https://static001.geekbang.org/resource/image/22/c6/22f170520f6cb6f1f786472b946b3fc6.png" alt="">
这张图是一个典型的音视频会议中服务器端录制的模型。下面我们就来看看它运转的过程:
- Browser 1 推送音视频流到 MedoozeMedooze使用 IncomingStream 代表这一路流。
- Medooze 将 IncomingStream 流分发给参会人 Browser 2 的 OutgoingStream并同时将流分发给 Recorder。
- Recorder 是一个录制模块,将实时 RTP 流转换成 MP4 格式,并保存到磁盘。
通过以上步骤Medooze就完成了会议中实时录制的功能。
如果,用户想回看录制好的多媒体文件,那么只需要按以下步骤操作即可:
- 首先,告诉 Medooze 的 Player 模块,之前录制好的 MP4 文件存放在哪个路径下。
- 接着Player 从磁盘读取 MP4 文件,并转换成 RTP 流推送给 IncomingStream。
- 然后Medooze 从 IncomingStream 获取 RTP流 并分发给 Browser 3 的 OutgoingStream。
- 最后,再经过 DTLSICETransport 模块传给浏览器。
这样就可以看到之前录制好的音视频了。
### 3. 推流模型
下面是 Medooze的推流模型
<img src="https://static001.geekbang.org/resource/image/1d/8e/1d5eb2f6019e8a1685f05a8f70dcf38e.png" alt="">
通过前面的讲解,再理解这个模型就非常简单了。下面让我们来看一下推流的场景吧:
- 首先,用 VLC 播放器推送一路 RTP 流到 Medooze。
- 然后Medooze 将此流分发给 Browser 2、Browser 3、Browser 4。
没错Medooze 推流的场景就是这么简单,只需要通过 FFMPEG/VLC 向 Medooze的某个房间推一路 RTP 流,这样加入到该房间的其他用户就可以看到推送的这路音视频流了。
学习了上面的知识后,你是不是对 Medooze 的基本功能以及运行流程已经了然于胸了呢现在是不是迫切地想知道Medooze内部是如何实现这些功能的那么下面我们就来分析一下Medooze的架构。
## Medooze架构分析
由于 Medooze 是一个复杂的流媒体系统,所以在它的开源目录下面有很多子项目来支撑这个系统。其中一个核心项目是 media-server它是 C++ 语言实现的,主要是以 API 的形式提供诸多流媒体功能,比如 RTMP 流、RTP 流、录制、回放、WebRTC 的支持等。另外一个项目是 media-server-node此项目是 Node.js 实现的,将 media-server 提供的 C++ API 包装成JavaScript接口以利于通过 Node.js 进行服务器业务控制逻辑的快速开发。
接下来,我们就以 media-server-node 作为切入点,对此服务展开分析。
### 1. 源码目录结构
media-server-node 是基于 Node.js 的一个项目,在实际开发过程中,只需要执行下面的命令就可以将 media-server-node 编译安装到你的系统上。
```
npm install media-server-node --save
```
这里我们对 media-server-node 源码目录结构做一个简要的说明,一方面是给你一个大概的介绍,另一方面也做一个铺垫,便于我们后续的分析。
<img src="https://static001.geekbang.org/resource/image/b8/ef/b871ba25a899ae1692fd67b75909e2ef.png" alt="">
通过对上面源码目录结构的分析,你可以看出它的结构还是蛮清晰的,当你要分析每一块的具体内容时,直接到相应的目录下查看具体实现就好了。
目录结构介绍完后,我们再来看看 Medooze 暴露的几个简单接口吧!
### 2. 对外接口介绍
media-server-node 采用了JavaScript ES6 的语法,各个模块都是导出的,所以每一个模块都可以供应用层调用。这里主要讲解一下 MediaServer 模块暴露的接口:
<img src="https://static001.geekbang.org/resource/image/9b/06/9b13d6ff2b8e113dc3b4011d2088d406.png" alt="">
如果你对JavaScript 非常精通的话你可能会觉得这几个接口实在太简单了但Medooze 设计这些接口的理念就是尽量简单,这也符合我们定义接口的原则。对于这些接口的使用我们会在后面的文章中做讲解,现在你只需要知道这几个接口的作用是什么就好了。
### 3. Medooze结构
讲到Medooze结构也许稍微有点儿抽象、枯燥不是那么有意思不过只要你多一点点耐心按照我的思路走一定可以将其理解的。
在正式讲解Medooze结构之前我们还需要介绍一个知识点在 Node.js中JavaScript 与 C++ 是如何相互调用的呢知道其原理对你理解Medooze的运行机制有非常大的帮助所以这里我做一些简单的介绍。JavaScript与C++调用关系图如下:
<img src="https://static001.geekbang.org/resource/image/d8/5d/d864639fb361e76993dd824a21b3085d.png" alt="">
我们都知道 Node.js 解析JavaScript脚本使用的是 Google V8 引擎,实际上 V8 引擎就是连接 JavaScript 与 C/C++ 的一个纽带,你也可以把它理解为一个代理。
在 Node.js 中我们可以根据自己的需要按V8引擎的规范为它增加C/C++模块,这样 Node.js就可以将我们写好的C++模块注册到V8 引擎中了。注册成功之后V8 引擎就知道我们这个模块都提供了哪些服务API
此时JavaScript 就可以通过 V8 引擎调用 C/C++ 编写的接口了。接口执行完后,再将执行的结果返回给 JavaScript。以上就是JavaScript 与 C/C++ 相互调用的基本原理。
了解了上面的原理后咱们言归正传还是回到我们上面讲的Medooze整体结构这块知识上来。从大的方面讲 Medooze 结构可以分为三大部分,分别是:
- **media server node**,主要是 ICE 协议层的实现,以及对 Media server Native API 的封装。
- **SWIG node addon**,主要是实现了 Node.js JavaScript 和 C++ Native 之间的融合,即可以让 JavaScript 直接调用 C/C++实现的接口。
- **media server**是Medooze 流媒体服务器代码的实现比如WebRTC协议栈、网络传输、编解码的实现等都是在该模块实现的。
让我们看一下 Medooze 整体结构中三个核心组件的作用,以及它们之间的相互依赖关系。如下图所示:
<img src="https://static001.geekbang.org/resource/image/4e/3e/4e280e6884f1a6e1b3123617c820e73e.png" alt="">
从这张图我们可以知道Medooze的整体结构也是蛮复杂的但若仔细观察其实也不是很难理解。下面我们就来详细描述一下这张图吧
- 图中标有 WebRTC 的组件实现了 WebRTC 协议栈,它是用于实现多方音视频会议的。
- Recorder 相关的组件是实现录制的,这里录制格式是 MP4 格式。
- Player 相关组件是实现录制回放的,可以将 MP4 文件推送到各个浏览器,当然是通过 WebRTC 协议了。
- Streamer 相关组件是接收 RTP 流的,你可以通过 VLC 客户端推送 RTP 流给 MedoozeMedooze 然后再推送给各个浏览器。
接下来,针对这些组件我们再做一个简要说明,不过这里我们就只介绍 Media server node 和 Media server 两大组件。由于 SWIG node addon 是一个 C++ Wrapper主要用于C/C++与JavaScript脚本接口之间的转换所以这里就不对它进行说明了。
需要注意的是,上图中的每一个组件基本是一个 Node.js 模块或者是一个 C++ 类也有极个别组件是代表了几个类或者模块比如SDP 其实是代表整个 SDP 协议的实现模块。
**Media server node 组件**中每个模块的说明如下:
<img src="https://static001.geekbang.org/resource/image/1c/ff/1c785c5fbf973db368fb84d5df1fecff.png" alt="">
了解了 Media server node 组件中的模块后,接下来我们再来看看 **Media server 组件**中每个模块的作用,说明如下:
<img src="https://static001.geekbang.org/resource/image/e3/20/e3d70f0bf305018f040f9c1fa0171a20.png" alt="">
## 什么是SWIG
SWIGSimplified Wrapper and Interface Generator 是一个将C/C++接口转换成其他脚本语言如JavaScript、Ruby、Perl、Tcl 和 Python的接口转换器可以将你想暴露的 C/C++ API 描述在一个后缀是 *.i 的文件里面然后通过SWIG编译器就可以生成对应脚本语言的Wrapper程序了这样对应的脚本语言如 JavaScript就可以直接调用 Wrapper 中的 API 了。
生成的代码可以在脚本语言的解释器平台上执行从而实现了脚本语言调用C/C++程序API的目的。SWIG只是代码生成工具它不需要依赖任何平台和库。
至于更详细的信息,你可以到 SWIG 的官网查看,地址在这里:[http://www.swig.org/](http://www.swig.org/) 。
## 小结
本文讲解了 Medooze 的 SFU 模型、录制回放模型、推流模型等内容并且介绍了Medooze的整体结构和核心组件的功能等。通过以上内容的讲解你应该对 WebRTC 服务器有一个初步认知了。
除此之外文章中也提到了WebRTC流媒体服务器的传输需要基于 DTLS-SRTP 协议以及 ICE 等相关识。这些知识在后面的文章中我们还会做更详细的介绍。
## 思考时间
通过上面的描述,你可以看到 Medooze是作为一个模块加入到 Node.js 中来提供流媒体服务器能力的,那么这种方式会不会因为 Node.js 的性能而影响到 Medooze 流媒体服务的性能呢?
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,140 @@
<audio id="audio" title="28 | 让我们一起探索Medooze的具体实现吧" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/07/ab/0736c4d802c42f151dfd2aa0a6c022ab.mp3"></audio>
在[上一篇文章](https://time.geekbang.org/column/article/136000)中,我向你介绍了 Medooze 的 SFU 模型、录制回放模型以及推流模型并且还向你展示了Medooze的架构以及Medooze核心组件的基本功能等相关方面的知识。通过这些内容你现在应该已经对Medooze有了一个初步了解了。
不过,那些知识我们是从静态的角度讲解的 Medooze 而本文我们再从动态的角度来分析一下它。讲解的主要内容包括WebRTC 终端是如何与 Meooze 建立连接的、数据流是如何流转的以及Medooze是如何进行异步I/O事件处理的。
## 异步I/O事件模型
异步I/O事件是什么概念呢你可以把它理解为一个引擎或动力源这里你可以类比汽车系统来更好地理解这个概念。
汽车的动力源是发动机,发动机供油后会产生动力,然后通过传动系统带动行车系统。同时,发动机会驱动发电机产生电能给蓄电池充电。汽车启动的时候,蓄电池会驱动起动机进行点火。
这里的汽车系统就是一种事件驱动模型发动机是事件源驱动整车运行。实际上Medooze的异步I/O事件模型与汽车系统的模型是很类似的。那接下来我们就看一下 Medooze 中的异步I/O事件驱动机制
<img src="https://static001.geekbang.org/resource/image/7f/6d/7f431dc7ae34e5aa635952b461ec726d.png" alt="">
上面这张图就是Modooze异步 I/O 事件处理机制的整体模型图,通过这张图你可以发现 **poll 就是整个系统的核心**。如果说 EventLoop 是 Medooze 的发动机,那么 EventLoop 中的 poll 就是汽缸,是动力的发源地。
下面我就向你介绍一下 **poll 的基本工作原理**。在Linux 系统中无论是对磁盘的操作还是对网络的操作它们都统一使用文件描述符fd来表示一个fd既可以是一个文件句柄也可以是一个网络socket的句柄。换句话说我们只要拿到这个句柄就可以对文件或socket进行操作了比如往fd中写数据或者从fd中读数据。
对于 poll 来说,它可以对一组 fd 进行监听,监听这些 fd 是否有事件发生实际上就是监听fd是否可**写数据**和**读数据**。当然具体是监听读事件还是写事件或其他什么事件是需要你告诉poll的这样poll一旦收到对应的事件时就会通知你该事件已经来了你可以做你想要做的事儿了。
那具体该怎么做呢?我们将要监听的 fd 设置好监听的事件后,交给 poll同时还可以给它设置一个超时时间poll就开始工作了。当有事件发生的时候poll 的调用线程就会被唤醒poll重新得到执行并返回执行结果。此时我们可以根据poll的返回值做出相应的处理了。
需要说明的是poll API 本身是一个同步系统调用当没有要监听的事件I/O 事件 和 timer 事件发生的时候poll 的调用线程就会被系统阻塞住并让它处于休眠状态而它处理的fd是异步调用这两者一定不要弄混了。
poll 的功能类似 select其API原型如下
```
include &lt;poll.h&gt;
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
```
其各个参数及返回值含义如下表所示:
<img src="https://static001.geekbang.org/resource/image/52/d5/52c20170b8a5cf27141b6ba535b6fed5.png" alt="">
其中第一个**fds ,它表示的是监听的 pollfd 类型的数组**pollfd 类型结构如下:
```
struct pollfd {
int fd; / file descriptor /
short events; / requested events /
short revents; / returned events /
};
```
- fd 是要监听的文件描述符。
- events 是一个输入参数,表明需要监听的事件集合。
- revents 是输出参数,表明函数返回后,在此 fd 上发生的事件的集合。
下表是常见事件的说明:
<img src="https://static001.geekbang.org/resource/image/cb/c3/cb6951789a5d0f73aa9ddc140edf15c3.png" alt="">
了解了 poll API 各参数和相关事件的含义之后接下来我们看一下当poll睡眠时在哪些情况下是可以被唤醒的。poll 被唤醒的两个条件:
1. 某个文件描述符有事件发生时;
1. 超时事件发生。
以上就是异步I/O事件处理的原理以及 poll API 的讲解与使用。接下来我们再来看看 EventLoop 模块中其他几个类的功能及其说明:
- **pipe**,好比是火花塞,用来唤醒 poll 的。比如 poll 线程被阻塞,我们又想让它随时可以唤起,就可以通过让 poll 监听一个 pipe 来实现。
- **Listener** 数据包处理接口。只要实现了 Listener 接口,当 fd 接收到数据包以后,就可以调用此接口来处理。
- **SendBuffer**Queue 数据包发送队列。当有数据包需要发送给客户端的时候,首先会发送到 EventLoop 的发送队列然后EventLoop 就会根据 fd 的 POLLOUT 事件进行分发。
- **Task Queue** 异步任务队列。当有逻辑需要 EventLoop 线程执行的的时候,通过调用 Async 函数将异步任务放入异步任务队列中,然后通过 Signal 唤醒 poll去处理此异步任务。
- **TimerImpl Queue** 定时器队列。需要定时器功能的时候,可以调用 CreateTimer/CancelTimer 去创建/取消一个定时器。
- **RTPBundleTransport**,实现了 Listener 接口,所有接收到的 UDP 数据包都由此类处理。
## ICE 连接建立过程
了解了Medooze的异步I/O事件处理机制后接下来我们看一下 ICE 连接的建立。实际上,传输层各个组件的基本功能,我们在[上篇文章](https://time.geekbang.org/column/article/136000)中都已一一介绍过了,不过那都是静态的,本文我们从动态的角度,进一步了解一下 DTLS 连接的建立过程。
<img src="https://static001.geekbang.org/resource/image/31/da/31a56c4adce92b82bf23453b2d0944da.png" alt="">
当某个参与人Participator )加入房间后,需要建立一个 DTLS 连接使客户端与Medooze可以进行通信上图描述了各个对象实例之间的先后调用关系。其具体连接建立过程说明如下步骤较多建议对照上图来理解和学习
1. 当有参与人Participator加入时如果此时房间内只有一个人则该 Participator 对象会调用 MediaServer::createEndpoint 接口,来创建一个接入点。
1. MediaServer 对象会创建一个 Endpoint 实例。
1. Endpoint 对象会创建一个 Native RTPBundleTransport 实例。
1. Native RTPBundleTransport 实例创建好后Endpoint 再调用 RTPBundleTransport 实例的init()方法对 RTPBundleTransport 实例进行初始化。
1. 在 RTPBundleTransport 初始化过程中,会创建 UDP socket动态绑定一个端口。这个动态端口的范围用户是可以指定的。
1. RTPBundleTransport 中包含了一个 EventLoop 实例,所以会创建事件循环实例,初始化 pipe。
1. EvenLoop 会创建一个事件循环线程。
1. 事件循环线程中会执行 poll进入事件循环逻辑。**此时Endpoint 的初始过程算是完成了**。
1. Participator 需要创建一个 **DTLS 连接**DTLS 连接是由 Endpoint::createTransport 函数来创建的。
1. Endpoint 创建一个 Transport 实例,此实例是 Native 的一个 wrapper。
1. Transport 调用 RTPBundleTransport::AddICETransport 接口,准备创建 Native DTLS 实例。
1. RTPBundleTransport 创建一个 DTLSICETransport 实例。
1. DTLSICETransport 聚合了 DTLSConnection 实例对其进行初始化。主要工作就是证书的生成、证书的应用、ssl 连接实例的创建以及DTLS 的握手过程。
1. DTLSICETransport 的初始化工作,包括 SRTPSession 的初始化和 DataChannel 的初始化。
经历这么一个过程ICE 连接就建立好了,剩下就是媒体流的转发了。
## SFU 数据包收发过程
当终端和 Medooze 建立好 DTLSICETransport 连接以后对于需要共享媒体的终端来说Medooze 会在服务端为它建立一个与之对应的 IncomingStream 实例,这样终端发送来的音视频流就进入到 IncomingStream中。
如果其他终端想观看共享终端的媒体流时Medooze 会为观看终端创建一个 OutgoingStream 实例,并且将此实例与共享者的 IncomingStream 实例绑定到一起这样从IncomingStream中收到的音视频流就被源源不断输送给了OutgoingStream。然后OutgoingStream又会将音视频流通过DTLSICETransport对象发送给观看终端这样就实现了媒体流的转发。
RTP 数据包在 Medooze 中的详细流转过程大致如下图所示:
<img src="https://static001.geekbang.org/resource/image/c8/e0/c8d913bc32dbe962ad3fad55b3251fe0.png" alt="">
图中带有 in 标签的绿色线条表示共享者数据包流入的方向,带有 out 标签的深红色线条表示经过 Medooze 分发以后转发给观看者的数据流出方向,带有 protect_rtp/unprotect_rtp 标签的红色线条表示 RTP 数据包的加密、解码过程,带有 dtls 标签的紫色线条表示 DTLS 协议的协商过程以及DTLS 消息的相关处理逻辑。
上面的内容有以下四点需要你注意一下:
- 如果采用 DTLS 协议进行网络传输,那么在连接建立的过程中会有一个 DTLS 握手的过程,这也是需要数据包收发处理的。所以,我们上图中的数据包流转不仅是音视频数据,还包括了 DTLS 握手数据包。
- DTLSConnection 是专门处理 DTLS 协议握手过程的,由于协议复杂,这里就不做详细介绍了。
- SRTPSession 是对音视频数据进行加密、解密的,这是 API 的调用,所以图里并没有流入、流出的过程说明。
- RTPLostPackets 主要是进行网络丢包统计。
接下来,我们就对数据包的流转过程做一个简要的说明(步骤较多,建议对照上图来理解和学习):
- 共享终端的音视频数据包到达 Medooze 服务器以后,是被 EventLoop 模块接收,接收到数据包不做处理,直接传给 RTPBundleTransport 模块。需要说明的是,数据包的收发最终都是落入到了 EventLoop 模块,因为数据包的收发都需要处理网络事件。
- 前面讲过RTPBundleTransport 模块就像一个容器,管理着所有参会终端的网络连接实例。当数据包到达 RTPBundleTransport 模块以后,它会根据数据包的来源,将数据包分发给相应的 ICERemoteCandidate 实例。ICERemoteCandidate 是针对每一个通信终端建立的一个逻辑连接。同样,收发也都需要经过 RTPBundleTransport 模块,因为此模块绑定了 UDP socket。
- ICERemoteCandidate 会对数据包的类型进行检查,如果是 DTLS 数据包,就会转给 DTLSConnection 模块,处理 DTLS 协商过程;如果是音视频数据,那么会转给 DTLSICETransport 模块。
- 在DTLSICETransport 处理模块中,它首先会调用 SRTPSession 的 unprotect_rtp 函数进行解密;然后,调用 RTPLostPackets 的 API 处理丢包逻辑最后将数据包传给RTPIncomingSourceGroup 模块。
- RTPIncomingSourceGroup 会遍历所有订阅此流的观看终端,然后将数据流转发给相应的 RTPOutgoingSourceGroup 实例。
- RTPOutgoingSourceGroup 模块对于下行流的处理,是通过 RTPStreamTransponder 模块进行 RTCP 相关逻辑处理,然后传给 DTLSICETransport 模块。
- DTLSICETransport 模块会调用 SRTPSession 模块的 protect_rtp 函数,对下行数据包进行加密,然后传给 RTPBundleTransport 模块。
- RTPBundleTransport 模块没有任何逻辑处理,直接将数据包传给了 EventLoop 模块。EventLoop 是提供了一个下行发送队列,其实是将数据保存到发送队列中。
- EventLoop 从发送队列中获取待发送的数据包并发送给观看端。
## 小结
本文我们重点介绍了事件驱动机制,它像人的心脏一样,在现代服务器中起着至关重要的作用。 Medooze 服务器中的具体实现模块就是 EventLoop 模块。通过本文的学习,我相信你对事件驱动机制有了一定的了解。接下来,就需要结合代码做进一步的学习,最好能进行相应的项目实践,这样对你继续研读 Medooze 源码会有很大的帮助。
后半部分,我们还分别介绍了 DTLS 连接建立过程和数据包的流转过程。通过本文以及上一篇文章的学习,我相信你对 Medooze 的基础架构以及 SFU 的实现细节已经有了深刻的理解。
## 思考时间
今天你的思考题是异步I/O事件中什么进候会触发写事件
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,176 @@
<audio id="audio" title="29 | 如何使用Medooze 实现多方视频会议?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/02/5a/0270e43cbb16491a8119394771eb4b5a.mp3"></audio>
前面我们通过两篇文章详细介绍了 Medooze 的实现逻辑,我相信你现在已经对 Medooze 有了非常深刻的认知。通过对Medooze实现原理和架构的了解我们可以知道Medooze支持的功能非常多如录制回放、推流、SFU 等,其中最主要的功能是 SFU 的实现。
实际上,**我们要实现的多方音视频会议就是由 SFU 来实现的或者你可以认为Medooz实现的 SFU 就是一个最简单的音视频会议系统**。Medooze 提供的 SFU Demo 的地址为:[https://github.com/medooze/sfu](https://github.com/medooze/sfu) 。
由于 SFU 是一个 Node.js 项目,所以在环境部署、源码分析的时候,需要你有 JavaScript 基础,也需要掌握 NPM 和 Node 的常用命令,比如 npm install 等。接下来我们主要从以下几方面向你介绍一下多方音视频会议SFU项目
- 搭建多方音视频会议系统,让你感受一下多方通信;
- 分析多方音视频会议实现的架构;
- 分析 Medooze API 的具体使用;
- 分析多方音视频会议的入会流程。
## 多方音视频会议环境的搭建
多方音视频会议SFU项目是纯 Node.js 实现的,可以说在目前所有的操作系统上都可以运行。然而,由于该系统依赖 medooze-media-server 项目,而该项目仅支持 macOS X 和 Linux 两种操作系统,所以 SFU 项目也只能在 macOS X 和 Linux 这两种操作系统下运行了。
本文我们所搭建的这个 Demo 的实验环境是在 Ubuntu 18.04 系统上,所以接下来的实验步骤和实验结论都是基于 Ubuntu 18.04 得出的,如果你在其他操作系统下搭建可能结果会略有不同,但具体步骤都是一模一样的。
下面我们就来分析下这个环境搭建的具体步骤。
第一步,打开 Linux 控制终端,下载 SFU 代码,命令如下:
```
git clone https://github.com/medooze/sfu.git
```
第二步,安装 SFU 依赖的所有包:
```
cd sfu
npm install
```
第三步,在 `sfu` 目录下生成自签名证书文件:
```
openssl req -sha256 -days 3650 -newkey rsa:1024 -nodes -new -x509 -keyout server.key -out server.cert
```
第四步,在 `sfu` 目录下启动服务:
```
node index.js IP
```
上面命令中的 IP 是测试机器的IP地址如果用内网测试你可以将它设置为 127.0.0.1。如果你想用外网访问则可以指定你的云主机的外网IP地址。另外需要注意的是该服务启动后默认监听的端口是 8084**所以你在访问该服务时要指定端口为8084**。
通过上面的步骤,我们就将 Medooze 的 SFU 服务器搭建好了,下面我们来测试一下。
打开浏览器,在地址栏中输入以下地址就可以打开 Medooze SFU Demo 的测试界面了:
```
https://IP:8084/index.html
```
在这个地址中需要特别注意的是,**必须使用 HTTPS 协议,否则浏览器无法访问本地音视频设备**。此外,这个地址中的 IP 指的是SFU服务的主机的IP地址可以是内网地址也可以是外网地址。
当该地址执行后,会弹出登录界面,如下所示:
<img src="https://static001.geekbang.org/resource/image/df/73/dfb0c325dfa9eef91cd56041259f2a73.png" alt="">
这是一个多人视频会议登录界面。关于登录界面,这里我们做一个简单的说明。
- 界面中的Room Id 和 Name 可以随机生成,也可以自己指定。如果需要随机生成,点击后面的 RANDOM 按钮即可。
- 几个参会人若想要进入同一个房间,那么他们的 Room Id 必须相同。
- 在 Audio Device 列出了所有麦克风,可供参会人选择。
- 在Audio Device下面有一个音量进度条它可以测试麦克风是否正常工作。
- 一切准备就绪后点击右下角的“READY!”按钮,就可以加入房间了。此时,在浏览器左下角显示了自己的本地视频。
- 由于这是基于浏览器的音视频应用,具有很好的跨平台性,因此你可选择 PC浏览器、手机浏览器一起测试。
上面我们就将 Medooze 的音视频会议系统的 Demo 环境搭建好了并且你可以进行多人的音视频互动了。接下来我们就分析一下这个音视频会议系统SFU Demo 是如何实现的吧。
## SFU 目录结构
首先我们来分析看一下 Medooze 的 SFU Demo 的目录结构,以及每个子目录中代码的作用,为我们后面的分析做好准备。
<img src="https://static001.geekbang.org/resource/image/06/29/06c09b7ebbee5cdb4e9c1de8ef42f629.png" alt="">
如上图所示,在 SFU Demo 目录下的文件非常少,我们来看一下每个文件或目录的具体作用吧!
<img src="https://static001.geekbang.org/resource/image/be/8e/becbb861d9775ad282513b3c451e268e.png" alt="">
了解了 SFU Demo 的目录结构及其作用后,我们再来看一下 SFU Demo 的架构是什么样子。
## SFU 架构
下图就是 Medooze SFU Demo 的架构图,你可以参考下:
<img src="https://static001.geekbang.org/resource/image/fc/e4/fc4bcb1763ec26052cfe9de5ca2e58e4.png" alt="">
从图中你可以看出SFU 架构包括了**浏览器客户端**和**SFU服务端**两部分。而SFU服务端又包括两部分功能WWW 服务、WebSocket信令及业务管理。
接下来我们就来分析一下**服务端的处理逻辑**。
Node.js中的 Web 服务器WWW是在 index.js 文件中实现的。当我们执行`node index.js IP`命令时,该命令启动了一个 HTTPS 服务,监听的端口是 8084。当 HTTPS 启动后,我们就可以从它的 WWW 服务获取到客户端代码了。需要说明的是HTTPS 服务依赖 server.key 和 server.crt 文件,这两个文件需要我们提前生成好(生成证书的命令在上面 SFU 环境搭建一节已经向你做了说明)。
至于WebSocket 服务及业务管理部分逻辑会稍微复杂些。服务端启动WebSocket服务后可以通过它来接收客户端发来的信令同时也可以通过它向客户端发送通知消息除了对信令的处理外SFU的业务逻辑也是在这部分实现的如 Room 和 Participator 实例的创建、参会人进入/离开房间等。
这里需要你注意的是WebSocket服务和业务管理的代码也是在 index.js中实现的。换句话说在服务端的 index.js 中,实现了 WWW 服务、WebSocket服务和SFU业务管理三块逻辑这样“大杂烩”的代码实现也只是在 Demo 中才能这么写了。另外index.js 的实现依赖了 websocket 和 transaction-manager 两个包。
分析完服务端的处理逻辑后,接下来我们再来看看**客户端**。浏览器首先从 Medooze SFU 的 WWW服务上获得 sfu.js 代码,在浏览器上运行 sfu.js就可以与 Medooze SFU 建立连接,并进行多方音视频通信了。在 sfu.js 中,主要实现了房间的登录、音频设备选择、采集音视频数据、共享音视频数据等功能。实际上,客户端的实现逻辑就是使用咱们专栏第一模块所介绍的知识,如 RTCPeerConnection 的建立、获取音视频流、媒体协商等。
此外Medooze SFU 还实现了几个信令,如 join 用于加入会议、update 用于通知客户端重新进行媒体协商等等这几个信令还是非常简单的不过却非常重要。尤其是在阅读Medooze SFU 代码时可以按照信令的流转来梳理SFU的代码逻辑这样会大大提高你的工作效率。
以上就是Medooze SFU 的基本架构,接下来我们看一下 SFU 的逻辑处理。
## SFU 逻辑处理
SFU 的实现很简单。在服务端有两个重要的类,即**Room** 和 **Participator**。它们分别抽象了多人会议中的**房间**和**参会人**。为便于你理解,我画了一个简单类图,如下所示:
<img src="https://static001.geekbang.org/resource/image/f2/ad/f23f501a0453d24157633c2a1e62abad.png" alt="">
从图中可以看到index.js 中定义了一个**全局对象 Rooms**保存多个房间Room实例。
**Room 类是对房间的抽象**
- Endpoint 属性是 Medooze Endpoint 类型;
- Participators 属性是一个 Map 类型,保存本房间中所有 Participator 实例;
- 该类还有两个重要方法createParticipator 用于创建参会人实例getStreams 获取所有参会人共享的媒体流。
**Participator 类是对参会人的抽象**
- IncomingStreams 属性是 Map 类型,保存自己共享的媒体流,元素属性类型是 Medooze::IncomingStream
- OutgoingStreams 属性是 Map 类型,保存自己观看的媒体流,元素属性类型是 Medooze::OutgoingStream
- transport 属性是 Medooze::Transport 类型,表示 DTLS 传输;
- init() 方法,初始化 transport 实例等;
- publishStream() 方法用于发布自己共享的流,创建 IncomingStream
- addStream() 方法用于观看其他参会人共享的流,创建 OutgoingStream 实例,并且 attachTo 到共享人的 IncomingStream 中。
## 入会流程
这里我们可以结合客户端和服务器的逻辑,分析一下参会人入会的大致流程,如下图所示:
<img src="https://static001.geekbang.org/resource/image/97/92/97b0ecb3c212a626f2e29ed1dab84c92.jpg" alt="">
图中深粉部分是客户端实现,在 `www/js/sfu.js` 中;绿色部分是服务器实现,在 `index.js` 中;橘色是在 medooze 中实现。那具体的流程是怎样的呢?
首先,浏览器从 Node.js 服务器获取到 `www/index.html` 页面及引用的 JavaScript 脚本,然后执行 connect 方法。在 connect 方法中使用 WebSocket 与SFU服务index.js建立连接。
服务端收到 WebSocket 连接请求后,从 URL 中解析到 roomid 参数。然后,根据该参数判断房间是否在之前已经创建过。如果该房间已创建,则什么都不做;如果没创建,则尝试创建 Room 实例。
对于客户端来说WebSocket 连接建立成功后,客户端会发送 join 信令到服务端。而服务端收到 join 信令后,创建 Participator参与人实例并且调用它的 init 方法进行初始化。
之后Participator 获取 Room 中所有已共享的媒体流,并调用它的 addStream 方法,这样它就可以订阅 Room 中所有的媒体流了。订阅成功后媒体流的数据就被源源不断地发送给Participator对应的终端了。
上面的逻辑完成之后,服务端会通知客户端 join 已经成功。此时,客户端也可以共享它的音视频流给服务器了。
此外,在客户端将自己的媒体流推送给服务端之前,服务器会调用 publishStream 方法在服务端创建 IncomingStream 实例,这样当客户端的数据到达服务器端后,就可以将数据交给 IncomingStream 暂存起来,以备后面分发给其他终端。
通过上面的步骤,新加入的客户端就可以看到会议中其他人分享的音视频流了,同时还可以将自己的音视频推送到服务器了。但此时,其他参会人还看不到新加入用户的音视频流,因为还有关键的一步没有完成。
这**关键的一步**是当新参会人一切准备就绪后服务端要发送广播消息update 通知所有已经在会中的其他人,有新用户进来了,需要重新进行媒体协商。**只有重新媒体协商之后,其他参会人才能看到新入会人的音视频流**。
以上步骤就是一个新参会者加入会议的大致过程,同时也介绍了这个过程中其他已经在会中的参与人需要如何处理。
## 小结
本文我们首先介绍了通过 Medooze SFU 如何搭建最简单的音视频会议系统,该系统搭建好后就可以进行多人音视频互动的“实验”了。
然后我们对Medooze 的 SFU 项目的逻辑做了详细介绍分析了Medooze SFU的基本架构、逻辑处理、入会流程等。相信通过本文的学习你对 Medooze 的使用和实现原理都会有更深刻的认识。
当然,如果你要想在自己的产品中更好地应用 Medooze还需要掌握更多的细节只有这样才能做到心中有数。比如如何能够承担百万用户级的负载如何为不同的的地区、不同的运营商的用户提供优质的服务这些都是你需要进一步研究和学习的。
## 思考时间
今天留给你的思考题:一个多人存在的会议中,又有一个人加入进来,其他参会人是如何看到后加入人的视频流的呢?你清楚其中的原理吗?
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,134 @@
<audio id="audio" title="30 | 实战演练通过WebRTC实现多人音视频实时互动直播系统" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/32/39/32b062a977026f65ebb2618e2cd90939.mp3"></audio>
关于通过WebRTC实现多人音视频实时互动的实战其实我们在[上一篇文章](https://time.geekbang.org/column/article/137836)中已经向你做过详细介绍了其中包括如何编译Medooze源码、如何将编译出的Medooze SFU进行布署以及如何去使用等相关的内容。
那么今天我们再从另外一个角度来总结一下 Medooze 是如何实现多人音视频互动的。
下面我们就从以下三个方面向你做一下介绍:
- 首先是多人音视频会议整体结构的讲解这会让你从整体上了解利用Medooze搭建的流媒体服务器与WebRTC客户端是如何结合到一起运转的
- 其次再对WebRTC客户端进行说明你将知道无论是Medooze还是使用其他的流媒体服务器对于客户端来讲它的处理流程基本是不变的
- 最后是Medooze服务器的总结让你了解Medooze各模块是如何协调工作的实际上这部分知识我们在之前的文章中已经做过介绍了但为了让你对它有更深刻的认识这里我们还会从另外一个角度再重新剖析。
## Medooze整体架构
文中接下来这张图清晰地展示了Medooze是如何实现多方通信的你可以先参考下
<img src="https://static001.geekbang.org/resource/image/07/c3/07198d161bdd52309a6aabcb507c99c3.png" alt="">
从这张图你可以看到,它主要分成两大部分:**服务端**和**客户端**。下面我们就从这两个方面向你详细描述一下 Medooze 实现多方通信的过程。
首先,我们来看一下**服务端都提供了哪些功能**。实际上,服务端主要提供了三方面的功能:
1. 提供了 HTTP/HTTPS 的 WWW 服务。也就是说我们将浏览器执行的客户端代码JavaScript放在 Node.js 的发布目录下,当用户想通过浏览器进行多方通信时,首先会向 Node.js 发起请求将客户端JavaScript代码下载下来。然后再与 Node.js 中的 WebSocket 服务连接。
1. 提供了 WebSocket 服务,主要用于信令通信。在我们这个系统中涉及到的信令并不多,有 join、update 以及 WebRTC需要交换的 Offer/ Answer、Candidate等几个简单的信令。对于这几个信令的具体作用我们会在本文的后半部分WebRTC客户端向你做详细介绍。
1. 提供了音视频流数据转发的能力。这块逻辑是使用 C++ 语言实现的,包括了 WebRTC 协议栈的实现、数据流的转发、最大带宽评估、防拥塞控制等功能。
接下来,我们就**从下载客户端代码开始详述一下WebRTC客户端与Medooze交互的整个过程**。
当进行多方通信时,用户首先要向 Node.js 发起HTTP/HTTPS请求然后从 Node.js 服务器上获取浏览器可以运行的 JavaScript 代码。浏览器获取到客户端代码后交由浏览器的 V8 引擎进行解析,然后调用 WebRTC 库相关的 API从本地摄像头和麦克风采集音视频数据以便后面将它们分享到Medooze服务器上这就是我们上图中**用红圈标出的第1步**。
当然,在正式分享音视频数据之前,客户端的 JavaScript 还要与 Node.js 之间建立 WebSocket 连接,也就是上图中**用红圈标出的第2步**。在 Medooze 多方通信实现过程中WebSocket Server 是由 Medooze 的 JavaScript 脚本来实现的当浏览器中的客户端与Node.js 中的 WebSocket 服务建立好连接后,客户端就会发送 **join** 信令到服务端。
服务端收到 join 信令后,会在它的管理模块中查找 join 要加入的房间是否存在,如果该房间已经存在,则将该用户加入到房间内;如果不存在,则在它的管理系统中创建一个新的房间,而后再将该用户加入到房间里。成功加入房间后,服务端会调用 media-server-node 中的 createEndpoint 接口,最终在 C++ 层打开一个UDP 端口为传输音视频数据做准备。
当前面这些准备工作就绪后客户端与服务端开始通过WebSokcet通道交换 SDP 信息包括非常多的内容比如双方使用的编解码器、WebRTC协议栈、Candidate等除此之外还包括 DTLS-ICE 相关的信息,通过这些信息客户端与服务器之间就可以建立起数据连接了。
接下来,我们再简要描述一下 **WebRTC 客户端与服务端之间是如何建立起 DTLS-ICE 连接的**
在双方交换了各自的 SDP 信息后,客户端就拿到了连接服务器的用户名和密码,即 ice-ufrag 和 ice-passwd 两个字段。其中, ice-ufrag 是用户名ice-passwd是连接的密码。当客户端与服务端建立连接时它要通过 STUN 协议向Medooze server 发送 Request 消息并带上ice-ufrag 和 ice-passwd这两个字段。 Medooze 服务端收到 STUN Request 消息后,将连接的用户名和密码与自己保存的用户名和密码做比较,如果一致,则说明该连接是有效的,这样客户端与服务端连接就建立起来了,并且后面所有从该客户端发到服务端的 UDP 数据包一律放行。
连接建立好后,为了保证数据安全,需要在客户端与服务器之间建立安全机制。这个安全机制就是我们在专栏第一模块分析“如何保障数据传输的安全”时所介绍的**证书和公钥的交换**。这一步执行完成之后,通信的双方就可以彼此传输音视频数据了。以上就是上图中**红圈标出的第3步**。
服务器在收到客户端发来的音视频数据后,并不能直接使用,还需要使用 SRTP 协议将加密后的数据进行解密,然后才能做后面的逻辑处理。
在音视频数据传输的过程中通信双方一直对传输的网络进行着监控。因为网络是变化的可能刚刚网络质量还特别好过一会儿就变得很差了。所以无论是Medooze还是其他的流媒体服务器对于网络质量的监控都是一项必备功能。**监控的方法也比较简单,就是通过 RTCP 协议每隔一段时间就上报一次数据**。上报的内容包括收了多少包、丢了多少包、延迟是多少等这些基本信息。有了这些基本信息后,在服务端就可以评估出目前网络的带宽是多少了。
以上的描述是其中一个参会人加入到房间里的情况,而对于多人来说,每个加入到会议里的人,都要做上面相同的逻辑。
以上就是Medooze多方通信整体结构的描述接下来我们再分别从客户端和服务端的角度详述它们都做了哪些事儿吧。
## WebRTC客户端
对WebRTC客户端来讲无论是1对1通信还是多方通信区别并不大。主要的逻辑像采集音视频数据、创建 RTCPeerConnection、媒体协商等都是一样的。只有在展示的时候稍有差别在1对1通信中只需要显示两个视频——本地视频和远端视频而多方通信时则要展示多个视频。可以说这是1对1通信与多方通信在客户端的唯一差别了。
文中接下来这张图是我总结的 WebRTC 客户端在多方通信中的流程图,你可以参考下:
<img src="https://static001.geekbang.org/resource/image/29/72/2923854cad3a56eb3342f6962f868772.png" alt="">
从图中可以看出WebRTC 客户端在多方通信中的基本处理逻辑是这样的:
- 首先创建 RTCPeerConnection 对象,用于与服务端传输音视频数据;
- 紧接着客户端与服务端建立 WebSocket 连接,建立好之后,双方就可以进行信令通信了;
- 当用户发送 join消息给服务器并成功加入到房间之后客户端就可以调用 getUserMedia 进行音视频数据的采集了;
- 数据采集到之后,要与之前创建好的 RTCPeerConnection 进行绑定然后才能通过RTCPeerConnection 实例创建Offer/Answer消息并与服务器端进行媒体协商
- 媒体协商完成后,客户端就可以将音视频数据源源不断发送给 Medooze 服务器了;
- 当有其他 WebRTC 客户端进入到房间后,它们的音视频流也会通过 RTCPeerConnection 传送给早已加入到房间里的 WebRTC终端此时 WebRTC 终端会收到 onRemoteTrack 消息,然后创建 HTML5 的`&lt;video&gt;`标签将它们显示出来就好了。
除此之外,在上图中,你应该还注意到一个 **update 信令**,这个信令在多方通信中是**至关重要**的。下面我们就来讲解一下update信令的作用。
在进行多方通信时,第一个 WebRTC 客户端已经加入到房间里了,接着第二个用户开始加入。当第二个用户加入时,它可以获得第一个用户共享的音视频流,但对于第一个用户来说,它是否能获得第二个用户的音视频流呢?显然获取不到。获取不到的原因是什么呢?因为第一个用户与服务器之间进行媒体协商时,它还不知道有第二个用户,这样当第二个用户进来时,如果不与服务器重新进行媒体协商的话,它是不知道房间里已经有其他人共享了音视频流的。
那如何解决这个问题呢?解决的办法就是**每当有新的用户进来之后,就通过 update 信令通知已经在房间内的所有用户,让它们重新与服务器进行媒体协商**。重新协商后,所有老用户就可以收到了新用户的视频流了。
以上就是客户端的基本处理逻辑。通过上面的讲解你可以了解到无论是多人互动还是1对1通信对于客户端来说基本逻辑都差不多。
WebRTC 客户端处理流程讲完后,接下来我们再来看看服务端。
## 服务端
文中接下来这张图是我总结的服务端架构图,你可以参考下:
<img src="https://static001.geekbang.org/resource/image/f4/5f/f480521496bf9c270a125d61095ecb5f.png" alt="">
如上图所示,从大的方面来讲,服务端由三部分组成,即 WebSocket 服务、SFU逻辑处理以及媒体服务器。如果再细化的话媒体服务器又是由 media-server-node 和 media-server 组成。
IncomingStream 和 OutgoingStream 是 media-server 的核心。就像之前的文章向你介绍的,你可以将 IncomingStream 和 OutgoingStream 看成是两个队列,其中 **IncomingStream 用于存储接收到的数据,而 OutgoingStream 用于存放要发送的数据**
在一个会议中,每个参会人一般只有一个 **IncomingStream 实例**用于接收该参会人共享的音视频流。但却可以有多个 **OutgoingStream实例**这每个OutgoingStream 实例表示你订阅的其他参会人共享的音视频流。如下图所示Browser2 和 Browser4 都在两路 OutgoingStream实例。
<img src="https://static001.geekbang.org/resource/image/7a/fe/7afd1da587c15ce0c965d453a2ceeafe.png" alt="">
上面这张图,我们在[《27 | 让我们一起探索Medooze的具体实现吧](https://time.geekbang.org/column/article/136000)一文中已经向你做过详细描述了,这里就不再赘述了。如果你记不清了,可以再去重温一下这篇文章。
通过 Medooze Server 架构图,你可以看到它有**两个网络接入点**:一个是信令接入点,也就是就是**WebSocket 服务**;另一个是音视频数据接入点,即 **Endpoint**。下面我们就对这两个接入点做一下介绍。
**WebSocket 服务**做的事件比较简单就是用来收发信令。像上面“WebRTC客户端”部分所介绍的那样在Medooze的 SFU Demo中它定义的信令消息特别少只有 join、update以及WebRTC 用于媒体协商的 Offer/Answer 等几个消息。
而每个 **Endpoint** 实际上就是一个 `&lt;IPport&gt;` 对,它使用的是 UDP 协议。对Medooze服务端的每个房间都会为之创建一个独立的 Endpoint用来接收/发送音视频数据。Endpoint接收的数据要复杂很多除了音视频数据外还有 STUN协议消息、DTLS协议消息、RTCP协议消息等。所以如果你想将Medooze流媒体服务器理解透彻就要花大量的时间先将上面这些协议搞清楚、弄明白。说实话这可不是一朝一夕的事儿。
另外为了使服务器在处理网络IO时更高效Medooze 使用异步IO的方式来管理 EndPoint。也就是使用的 poll API对 Endpoint(socket)做处理。关于这一点我们在[《28 | 让我们一起探索Medooze的具体实现吧](https://time.geekbang.org/column/article/136862)一文中已经做了全面的介绍,如果记不清了,你可以再重新回顾一下那篇文章。
当然 Medooze 使用poll 来处理异步IO事件性能还是低了点其实更好的方式是使用 epoll API这个API 在 Linux 下是最高效的或者可以使用开源的异步IO事件处理库如 libevent/libuv 等,因为它们底层也是使用的 epoll。
接下来,我们再来看一下**服务端通过 Endpoint 从客户端收到数据包后的处理逻辑是怎样的**。
像前面我们介绍过的,从 Endpoint 收到的数据包包括 STUN 消息、DTLS 消息、RTCP/SRTCP 消息、音视数据等多种类型。所以 Medooze 后面的处理逻辑非常复杂,涉及到很多细节,这需要你一点一点去理解。但大体的脉落还是比较清晰的,如下图所示:
<img src="https://static001.geekbang.org/resource/image/e5/60/e5cd470683c0b5f41bc8ce5c0f3db560.png" alt="">
上图就是Medooze处理各种数据包的时序图。客户端首先发STUN Request 包,服务端收到该请求包后,对客户端的身份做验证,检查该客户端是否是一个合法用户,之后给客户端回 Response 消息。
如果客户端是合法用户则发送DTLS消息与服务端进行 DTLS “握手”。在“握手”过程中要交换双方的安全证书和公钥等相关信息,也就是时序图中的 Client Hello、Server Hello、Finished 等消息。
证书交换完成后服务端和客户端都获得了协商好的加密算法等信息此时就可以传输音视频数据包了。服务端在收到数据包后还要调用SRTP库进行解密也就是将SRTP消息解密为 RTP 消息,最终获取到用户的 RTP 音视频消息后,就可以进行后面的操作了。
以上就是 Medooze 流媒体服务器大致的处理逻辑。
## 小结
本文我们从 Medooze 的整体结构、WebRTC客户端处理流程以及 Medooze 流媒体服务器三个方面向你详细介绍了WebRTC是如何实现多人实时互动直播的。这篇文章是一个总结性的文章其主要目的是带你梳理一下我们前面所讲的知识点通过本文你会对 Medooze 实现多方通信有一个全局的理解。
另外Medooze 实现多方通信涉及到很多细节需要你花大量的时间去阅读代码才可以将它们弄明白、搞清楚而我们专栏中的几篇文章就是给你讲清楚Medooze在实现多方通信时的主脉络是什么以便让你更加容易地去理解Medooze的代码实现。
在阅读Medooze源码时建议你先按照[上一篇文章](https://time.geekbang.org/column/article/137836)中的步骤将 Medooze Demo 环境搭建出来,然后再以 Medooze分析的几篇文章为主线对Medooze的源码进行阅读和实验这样你很快就可以将Medooze的细节搞清楚了。
至此我们专栏的第二模块“WebRTC多人音视频实时通话”就讲解完了。
## 思考时间
今天我留给你的思考题是Medooze为什么要使用 STUN/DTLS 等协议?可以不用吗?
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。