基于Nginx的媒体服务器技术

 

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

文 / 朱建平nginx

整理 / LiveVideoStackgit

直播回放github

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的播放与拉流。session

一样,咱们能够根据上述思路继续扩展协议。假如咱们在收到一个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的需求,此时对于内存的利用率是很是高的。

2. 静态推拉流

 

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

 

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

 

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

 

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

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

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

 

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的所有自定义参数传递过来。

4. 多进程:进程间回源

 

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

5. 更多操做说明

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