基于 Nginx 的媒体服务器技术

2020-03-25 10:53:02 +08:00
 baoziyan

国内应用比较多的开源流媒体服务器 nginx-rtmp-module 一直存在功能少、集群化难度大等问题。在 LiveVideoStack 线上分享中,PingOS 开源项目组开发工程师、UCloud RTC 研发工程师朱建平详细介绍了基于 nginx-rtmp-module 的 PingOS 流媒体服务器在 http-flv 、http-ts 、hls+、多进程、转推、回源以及集群化部署方面的技术实现细节。 文 / 朱建平

整理 / LiveVideoStack

直播回放

https://www2.tutormeetplus.com/v2/render/playback?mode=playback&token=006643cdea15499d96f19ab676924e88

  1. Nginx 流媒体扩展: http-flv 、http-ts 、hls+

最初始的 nginx-rtmp-module 相关模型与包括 SRS 在内的多数流媒体服务器实际上是一样的( 1 个生产者,n 个消费者)。Nginx 存一个问题:它仅仅做了 RTMP 的消费模型,如果想扩展 http-flv 或 http-ts 的形式会较为困难。由于 rtmp-session 仅供 RTMP 协议使用,如果想扩展 http-flv,首先我们需要了解其基础分发模型(如上图所示):所有的生产者与消费者都会被挂载到同一个 stream 中,生产者负责从网络端接收数据,消费者从 buffer 中获取数据对外发送。

如果是发送 flv 数据,那么可以保留原有 rtmp-session,当服务器收到一个 HTTP 请求时,创建一个 rtmp-session,此 session 与网络不相关,仅仅是逻辑上的 session 。然后将这个 session 注入 stream 当中,如果是以消费者的角色注入进 stream 当中,则可以实现获取数据并往外分发。

假如此时服务器收到的是 http-flv 的请求,就可以创建一个逻辑上的 session,并把它注入 stream 中,此时理论上我们可以获得的是 rtmp 的数据。但我们需要的是 flv 的数据,由于 flv 数据与 rtmp 数据相似,我们可以通过 tag-header 的方式非常简单的将 rtmp 数据还原成 flv 数据。

根据上述思路,在生产者和消费者模型中,消费者可以通过创建 http-fake-session 的形式来复用以前的分发流程并实现 http-flv 协议。我们对其进行扩展,创建一个 http-fake-session 作为生产者,并让 http-fake-session 与一个 http client 进行关联,关联之后 http client 负责从远程服务器端下载数据传递给生产者,生产者就可以把这些数据通过分发模型分发给下面的 rtmp-session 。这样也就间接实现了一个 http 回源的功能。通过上述思路我们就能够快速地实现 http-flv 的播放与拉流。

同样,我们可以根据上述思路继续扩展协议。假如我们在收到一个 http 请求之后,创建一个同样的 rtmp-fake-session (逻辑上的 session,与网络不相关),我们把它以消费者的角色插入到 stream 当中。这样就可以从 stream 当中获取到需要向下分发的数据。需要注意的是:stream 中最初保存的是 rtmp 数据而不是 ts 数据,无法直接获取 ts 数据。

1.1 http-flv 在 Nginx 中的实现

基于 Nginx 实现 http-flv 需要注意以下几点细节:首先该实现复用了 Nginx 的分发模型以及 http 功能模块。( Nginx 对 http 协议栈的支持更加完善,包括 http1.0 、http1.1 协议)

在部分线上业务中,客户可能需要在下载 http-flv 时添加后缀,按照以往的实践逻辑我们会在代码当中过滤后缀。如果遇见更为复杂,如修改是否需要开启 http chunked 编码的需求,我们就只能修改代码。而如果是基于 Nginx 通过复用 http 的现有模块来实现 http-flv,我们就可以通过 nginx-http-rewrite 功能来实现这些操作。因此使用 nginx-http 的原生功能来开发 http-flv 可以带来更多好处,如显著降低代码量。

在这里我曾经看到过一种情况:即复用了 http 模块,但没有复用 rtmp 的分发流程。这样就会导致我们需要将分发流程在 http-flv 中重新再做一遍,对业务的控制就会变得非常复杂。举个例子,假如此时有人请求播放,需要将消息通知给业务服务器。此时,如果 rtmp 与 http-flv 两种协议的实现是分开的,那么意味着如果两者都被触发,就需要分别向业务服务器进行汇报。于是我们就需要付出双倍的代码与逻辑维护工作,这无疑会显著增加开发与维护成本。

因此,最简单的实现方案就是 flv 不做任何与业务相关的处理,仅在下发的时候进行格式转换,相当于 rtmp 分发时只发 rtmp 格式的数据,而 flv 分发时只需要将 rtmp 的数据打上 flv 的 tag-header,然后再进行下发,这样就省去了业务层的开发。

http-flv 播放实现

图中展示的是 rtmp 的缓存对于 rtmp 和 http-flv 这两个协议的支持。http-flv 和 rtmp 二者共用一套缓存,其实 rtmp 本身传输的就是 flv 的数据,只不过是把 tag-header 给抛掉了。http-flv 的下发与 rtmp 的下发唯一的区别点在于 send 函数不同: http-flv 调用的是 http 的 send 函数,rtmp 下发时调用的是原生的 send 函数,在下发前需要添加各自的协议头。二者共用一块内存可以达到节省内存的效果,并且实现业务统一,降低开发成本。

http-flv 回源的实现

图中展示的是 http-flv 回源在 nginx 中的实现。http-flv 回源实现的思路与 http-flv 的播放实现思路类似:即在需要回源的时候创建一个 http client,http client 所做的事情就是把 http 数据下载到本地。在下载数据到本地之前 http client 需要先创建一个 rtmp fake session 并将其作为生产者注入 stream 当中。而后 http client 开始从网络上下载数据并且将下载到的 fIv 数据拆成 rtmp 数据。

为什么要拆成 rtmp 数据?这是因为 rtmp 的推流过来的缓存数据类型是 rtmp,因此从网上下载到的 flv 数据需要做一次拆分,拆成 rtmp 的数据,然后放入缓存。最终根据实际要求将数据转成 rtmp 或 flv 的格式。这样按照 http-flv 播放中 rtmp fake session 的逻辑,也就能够快速的实现 http-flv 的回源操作。

1.2 http-ts 在 Nginx 中的实现

图中展示的是 http-ts 在 Nginx 中的实现。其实现思想与 http-flv 的实现基本一致,仅仅是在操作上有所不同,不同点在于 http-ts 需要一个独立的 buffer 进行缓存。由于 http-ts 与 http-flv 的数据格式相差较大,对于 flv 数据到 rtmp 来说,只需要将数据拆成一个个小块,并在前面添加一个 header 。即使 flv 数据的最一帧或者一个分块缺少也不用补齐。

但是 ts 数据不同,它的要求比较严格,每一分块必须为 188 字节,其中包括 ts header 以及有效载荷部分。并且如果数据库大小不足 188 个字节,则需要补齐。而 rtmp 的数据块没有严格固定要求其长度大小。对于 ts 数据来说,要想将 flv 数据转成 ts 数据,这个过程是需要消耗一些计算量的。

由于 ts 数据和 flv 的数据格式相差太大,因此在这里我们将 ts 的 buffer 与 rtmp 的 buffer 完全独立开。但此操作并不是默认开启的,需要在服务器中进行配置。开启配置后,才会将 rtmp 的 buffer 生成一份镜像的 ts 数据,这一部分的 ts 数据仅会供 http-ts 和 hls 两个协议使用。服务器中还涉及到一个原生的 hls 服务,在这里我们没有做任何的改动,而是加入了 hls+的服务来使用这个 buffer 。

无论是 ts 还是 hls+,它们都注册了自己的 fake session,这样做的目的是为了统一业务。例如在有播放请求进入时,我们需要让业务服务器知道当前有请求产生。类似这种网络通知、事件通知的接口,在开发的过程中大家都希望只需要编写一份业务数据,而不是说做 hls 协议要针对 hls 播放写一个通知,做 ts 协议还要针对 ts 再写一份通知,这样业务代码会越来越庞大,最后导致服务几乎就很难维护。因此 fakesession 的作用是非常大的,其会把网络层与业务层完全隔离开。即使服务器本身的下发协议不是 rtmp,创建一个 rtmp-session 并挂载到业务服务器中即可。

总的来说,http-ts 与 http-flv 唯一实现区别就是获取 buffer 的位置不同。http-flv 需要从 rtmp buffer 获取,http-ts 则是从 ts buffer 中获取。

如果能理解 http-flv 的协议流程,那么也就不难理解 http-ts 的实现流程。

1.3 hls+在 Nginx 中的实现

图中展示的是 hls+在 nginx 中的实现。hls+与传统 hls 不同,传统 hls 在服务端没有状态,服务端包含大量碎数据,客户端在不断执行下载,而 hls+则会记录每一个客户端的状态。

对于如何记录每个客户端的状态,之前我曾尝试通过对 hls+的连接创建一个虚拟连接用来记录状态。但是发现业务会比较复杂,并且后期会存在很多问题,包括代码量、bug 以及维护成本等。于是更换另外一种思路,还是用 fake session 的方式来实现。利用 fake session 作为消费者放入,根据每次进入的 http,连接,通过 session ID 进行绑定。由于第 1 次发送 hls 请求时客户端是不知道 sessionID 的,如果服务器获取到一个没有 session ID 的连接,则认为此客户端为第 1 次进入。客户端会接收到一个 302 的回复,302 回复中会告诉客户端一个新的地址,其中包含一个 session ID 。客户端得到 session ID 之后,再次请求 m3u8 时,会加入 session ID,服务器就可获取相应 session ID 并对客户端进行身份区分。这样就能够通过 session ID 记录每一个客户端的播放状态。

为什么要记录这个状态?这主要是因为服务器不是将数据直接写入硬盘而是放进内存,它需要知道每一个用户、每一个客户端的下载进度,并根据不同的进度从内存中定位 ts 数据。hls+和 http-ts 它们共用了一个 ts buffer,并且 hls+是实时的从 buffer 中定位 ts 内容。所以对于 hls+来说,并没有真正的 ts 数据产生,只是记录每一个文件在内存里面的偏移量。因此 hls+不存在读写的问题,在做 hls 服务时,以前可能会遇到过一个问题——读写硬盘的瓶颈。机械硬盘的读写速度比较慢,普遍的解决思路就是挂载一个虚拟硬盘,将内存映射到目录中进行读写。如果采用的是 hls+的方案,就可以省去挂载的操作,对于内存也并没有太多的消耗。而且如果同时有 hls+以及 http-ts 的需求,此时对于内存的利用率是非常高的。

  1. 静态推拉流

静态推拉流主要是为了满足集群化的需求。如果单台服务器不足以支撑服务的高并发量,那么我们就需要考虑服务器的扩展性。除此之外如果用户分散在全国各地,还需要进行服务器之间的打通。但是如果业务没有那么复杂就可以选择使用静态推拉流。

静态推拉流服务配置如上图所示,首先看静态拉流:首先存在一个目标源站,如果使用静态回源,那么目标地址会被配置在配置文件当中,目标源站能随意更改。

图中展示的是一个简单的静态拉流模型:如果来自主播的数据被推流到源站 A,那么我们需要保证服务器 A 的地址不会改变。

除此之外,如果想要构建一套完善的流媒体系统,则需要包含静态拉流与静态推流。假如有观众向服务器 C 请求播放,那么服务器 C 就会向服务器 A 拉流,无论服务器 A 是否存在视频流,服务器 C 都会拉取。因此该模型只适用于较为简单的业务场景。

  1. 动态控制:动态回源、动态转推、鉴权

相对于静态推拉流的“无脑”推拉流,更适用于多数人需求的则是动态推拉流。

Nginx 的 RTMP 服务针对每一项功能都做了不同的触发阶段。以 oclp_play 为例,当有人启动播放时会触发 play 消息,play 消息会携带一项 start 参数。在播放过程中,play 消息依旧会被触发,只不过此时还会携带 update 参数。在 play 结束时也会触发一个 play 消息,所携带的参数是 down 。借助这些参数,我们可以实现向业务服务器通知请求播放以及播放的具体阶段。

3.1 动态回源

推流过程也存在类似操作,推流中存在 publish,同样分为三个阶段,play 和 publish 主要应用在鉴权操作中。如果在 start 阶段,业务服务器返回了一个 404 或者非 200 的结果,服务器就会中断当前的 play 请求,publish 亦是如此。除此之外,pull 与 push 主要应用于动态拉流阶段。当服务器接收到 play 请求,并且发现当前服务器里面没有目标流,也就是说 publish 的流不存在,就会触发 pull 的 start 阶段。在发送 start 请求之后如果业务服务器返回结果为 302,并且在 location 中又写了一个新的 rtmp 地址或 http-flv 地址,这台服务器就会向标记的那一台目标服务器拉取 rtmp 流或 fIv 流,这个过程就被称为动态拉流。

3.2 动态转推

与动态拉流相对应的是动态推流,其理解方式与动态拉流大致相同。如果你向服务器推流,服务器会向配置好的目标地址发送 start 请求。如果在返回结果当中加入一个新的 rtmp 地址,这一台媒体服务器就会向新的 rtmp 地址推流,这也就是动态推流的操作。

这一切的前提是返回 302 的结果,如果不想将流推出,那么反馈给服务器 400 或其他非 200,该流就会被中断。Oclp_stream 用的比较少,仅仅在这路流创建与消失时被触发。不管是 play 还是 publish,如果只有 play 或 publish 存在,都会认为这路流的生命周期还没有结束,只有当二者全部消失时才会被认定该路流生命周期已结束。同样的,如果一路流没有被发布过而是仅仅第一次有人请求,此时也会触发 start 并认为是该路流被创建,只不过没有生产者而已。这种场景的应用比较少,只有对业务要求比较高的系统可能会用到这一条消息。

上图展示了一个配置事例,主要包括查询服务器的 IP 、查询服务器 play 操作希望支持哪些阶段等。

集群化部署依赖业务(调度)服务器,如果有回源需求则让边缘服务器 B 在 oclp_hold 阶段向业务服务器查询,此时业务服务器会告诉边缘服务器 B 一个 302 地址,其中包含源地址。边缘服务器 B 就会从标记出来的这一台(媒体服务器 A )拉流,从而实现动态回源。

动态转推主要是为了把本地的流推出去。在 CDN 的服务中,不同集群负责不同的职能。例如有些集群负责录制,有些则仅负责转码,此时我们希望核心机器能够把这些需要转码或需要录制的流按照需求转接到相应集群。动态转推非常重要,如果业务中包含这些不同的类型,就需要添加配置 oclp_push 去实现动态转推。

3.3 鉴权

鉴权操作中,我们只会对 publish 或 play 进行鉴权。

如果 play 的时候反馈 200 就是允许播放,如果反馈 403 就是不允许播放,publish 也是如此,通过业务服务器控制客户某一次服务请求是否能被允许。

前端进行 play 或者是进行 publish 时,如何把鉴权的 token 带过来?

主要通过变量:args=k=v&pargs=$pargs

在向外发送 play 查询时,如果加入 args=k=v&pargs=$pargs,发请求时会带上这些参数,这样就可以将 rtmp 的全部自定义参数传递过来。

  1. 多进程:进程间回源

多进程问题在原生的 nginx rtmp 中有很多 bug,现在的做法是通过共享内存记录下每个进程上的 stream 列表。如果 play 的进程没有流,则查询 stream 列表,并通过 unix socket 向目标进程回源拉流。除此之外,进程间的回源不会触发 ocl_playoclp_publish oclp_pull 消息。

  1. 更多操作说明

PingOS: https://github.com/im-pingo/pingos

1065 次点击
所在节点    问与答
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/655958

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX