RTMP多媒体视频服务器(RTMP系列三)
一、轻量级RTMP多媒体视频服务器的设计
基于RTMP协议的流媒体视频服务器基于Librtmp开源库,以及根据Adobe公司公布的RTMP协议文档来设计的一个可以提供基础RTMP数据中继服务的轻量级多媒体服务器。基于RTMP协议的流媒体视频服务器可以通过网络与客户端相连,可以对用户感兴趣的监控场所进行监控,主要包含网络实时数据传输模块以及RTMP通信模块等,可以调用视频采集模块。
轻量级RTMP多媒体视频服务器的设计考虑到了以下几点:
第一,视频的清晰度。在保证提供清晰视频监控的前提下,需要考虑网络传输的带宽,编码质量的好坏,不同的设置会导致不同的传输的效果。视频压缩所选取的编码库以及网络状况的好坏会对视频的清晰度造成显著的影响。
第二,视频传输的连续性。采集视频模块需要连续稳定的保持采集,压缩编码模块的稳定压缩,传输模块的稳定传输,是保持视频监控图像的连续性的关键所在。
第三,视频传输的实时性。获取实时性的视频流,在硬件能力达到实时编码的前提下,需要控制数据实时的发送、到达、被处理,还需设置获取帧率与延迟的关系,保证传输编码打包好的FLV数据的速率与原始采集端采集YUV数据的速率一致。
系统的总体设计如图1-1所示。
采集终端与视频服务器设计在了一起,客户端通过网络获取监控数据构成了实时视频监控系统。服务器暂时采用基于Ubuntu系统的主机作为硬件支持(可以方便的移植到运行Linux系统的嵌入式设备上面,也可以通过JNI实现在Android系统的移植)。在其上构建一个视频采集压缩模块与RTMP网络服务器,由采集端将采集的YUV数据压缩为H264数据然后通过服务器封装成FLV格式,最后基于RTMP协议将数据传输至监控终端。客户端基于Linux操作系统以及Android系统。
服务器主要功能是监听客户端的连接请求,然后调用采集和编码模块,将视频数据发送给客户端。服务器开始工作后,就会监听和客户端预定的端口(通常情况下RTMP视频服务器绑定的监听端口为1935),按照RTMP协议与客户端完成握手传输播放功能,响应客户端的请求。
根据服务器中不同模块的功能,可以将服务器框架分为控制层、处理层以及数据层3个层次。主要由五部分组成的,分别是数据层的视频采集模块,处理层的视频编码模块、封装模块以及目标跟踪模块,控制层的主控模块,其中最重要的是监听端口与控制模块,此模块基于RTMP协议,完成客户端与服务器的交互过程。模块设计如图1-2所示。
监听与控制模块设计
此模块是根据RTMP协议的对特定端口进行监听(为1935端口,此端口为RTMP协议通信的默认端口)的交互模块,完成客户端与服务器的交互过程,最后实现基于RTMP的数据传输。
下面介绍监听与控制模块的运行流程图,如图1-3所示。
在启动服务器后,RTMP视频监控服务器的监听与控制模块会监听指定端口,响应客户端的需求。连接到第一个客户端时,开启视频采集模块,实时的将采集到的视频封装成FLV数据流格式,同时将采集好的数据去除Nalu起始码保存到视频缓存区,最后将FLV数据流通过RTMP协议发送给客户端。如果不是第一个客户端,服务器会直接从视频缓冲区将视频数据发给后面的用户,实现多客户的视频监控。
二、轻量级RTMP流媒体视频服务器的实现
RTMP流媒体服务器的实现基于Librtmp和RTMP协议,服务器主要包含视频采集编码模块以及基于RTMP协议的网络服务器模块,视频采集编码模块的实现已经在上一节中讲解,本节主要讲解网络服务器模块的实现。
本模块支持Flash播放协议,并实现输出H264直播流的RTMP服务器功能。通过Adobe公司提供的《rtmp specification 1.0》,归纳出RTMP服务器和Flash播放器客户端之间的交互流程。
首先,双方运行之后,客户端会向服务器发送握手的请求,服务器收到握手请求后,会依据RTMP协议的要求,完成检测版本号等操作,最后完成握手。在完成握手之后,客户端会继续向服务器发送连接的请求,服务器的服务层收到此消息后,会对其进行解析,此消息包含与服务器建立一个RTMP连接的请求,服务器按照消息中参数的数据,与客户端建立这个连接。在完成RTMP连接之后,客户端会继续向服务器发送创建流的请求,流可以传输音视频数据。服务器完成流创建之后,会发送消息给客户端。最后,客户端向服务器发送播放的命令。
按照RTMP协议的规范,处理层需要响应和处理的流程如图1-4所示。
RTMP是基于TCP协议的多媒体传输协议,因此RTMP服务器本质上是一个TCP服务器,它的逻辑结构基本上和普通的TCP服务器是类似的。基于TCP服务器架构原理,设计了RTMP网络服务器模块的运行流程如图1-5所示。
图1-5 RTMP网络服务器模块运行流程图根据开源项目RTMPDUMP,按照给出的RTMP网络服务器模块的运行流程图,我们接下来详叙RTMP服务器的具体实现。
首先,我们定义了RTMP_REQUEST结构体数据defaultRTMPRequest,此结构体包含服务器绑定的网络地址以及网络端口等参数,对defaultRTMPRequest进行初始化,设置网络地址和监听端口,这里我们选择1935端口,这也是RTMP协议中推荐的端口。
接下来在startServer()函数中,我们首先需要定义一个套接字,因为RTMP最终在网络中传输数据使用的就是Socket套接字传输。我们定义一个套接字,并绑定起先设置好的网络地址与端口号,进入监听状态,等待客户端请求的到来。为了方便传递多个参数到下一个函数,创建了STREAMING_SERVER结构体数据server,其中包含服务器运行状态信息以及我们刚刚与网络端口绑定的套接字信息等。Socket通信中的监听是一个阻塞的过程,程序会在accept处等待连接请求的到来,所以需要启动服务器线程,并将server数据传入其中。
服务器线程运行函数acceptThread(),循环等待客户端的连接请求,并将服务器状态设置为STREAMING_ACCEPTING。当accept到连接请求,首先记录客户端的数量,并创建新的Socket与客户端的信息绑定。此时,服务器与客户端的TCP连接已经建立起来。由于要实现支持多客户端,我们在这里开启服务线程Server,并将server数据与客户端的套接字描述符sockfd一并传入到新的线程里面。While循环继续工作,等待新的客户端连接请求的到来。其中核心代码如下:
<pre>
while (server->state == STREAMING_ACCEPTING)
{
struct sockaddr_in addr;
socklen_t addrlen = sizeof(struct sockaddr_in);
int sockfd =accept(server->socket, (struct sockaddr *) &addr, &addrlen);
control.client_num++;
RTMP_LogPrintf("client:%d\n",control.client_num);
if (sockfd > 0)
{
paras.server=server;
paras.sockfd=sockfd;
ThreadCreate(Server, ¶s);
//doServe(server, sockfd);
RTMP_Log(RTMP_LOGDEBUG, "%s: processed request\n", FUNCTION);
}
else
{
RTMP_Log(RTMP_LOGERROR, "%s: accept failed", FUNCTION);
}
}
</pre>
新的线程中运行了Server()函数,需要调用函数完成实现RTMP流媒体传输的握手、连接、创建流以及播放等过程。
1.首先,调用RTMP_Serve()来实现RTMP协议中的握手内容
调用RTMP_Serve()函数需要传递RTMP结构体数据。RTMP结构体主要记录了输出输入块大小、流ID、时间戳等一系列信息。我们定义了RTMP结构体指针rtmp,对其申请空间并进行初始化,定义RTMP包结构体packet,用来储存RTMP协议的数据单元。
RTMP握手过程如图1-6所示,具体流程如下:
(1)首先服务器和客户端处于未初始化状态(Uninitialized),客户端开始发送C0、C1块。其中C0包含协议的版本号。服务器如果支持这版本,则收到C0或者C1块之后,会向客户端发送S0和S1,如果不支持这个版本,服务器就会终止连接。
(2)完成初始化后处于发送状态(Version Sent),当客户端收齐S0和S1数据之后,就会向服务器发送C2块。同时服务器等待C1的到来,接收之后,服务器发送数据块S2,此时服务器和客户端都进入了确认已发送状态(Ack Sent)。
(3)服务器端和客户端分别接收到S2和C2块之后,就进入握手结束状态(Handshake Done),到此,客户端和服务器完成了整个握手过程。
在RTMP_Serve()函数中,服务器实际上调用了SHandShake()函数来完成与客户端的握手过程。握手开始后,服务器端首先通过ReadN()函数读取接收到的1个字节数据,分析该数据,若确认为按照协议发送的C0块,则从该数据中提取出RTMP版本,如果服务器支持该版本,则继续等待接收下一个消息,如果不支持,服务器则终止连接。此时完成与客户端的第一次握手。之后再记录服务器的本地时间戳,然后通过WriteN()函数向客户端发送S0和S1数据,然后等待C1数据的到来,当ReadN()读取到C1,服务器按照解析C0的步骤,再次对C1的数据进行解析,完成第二次握手。最后再通过WriteN()发送S2,等待ReadN()读取客户端发送的C2,当接收到C2,则完成第三次握手,此时整个握手阶段完成。
2.其次,在保证一直与客户端连接的情况下,完成创建RTMP流媒体的连接、创建流和播放操作。
关键代码如下:
<pre>
while (RTMP_IsConnected(rtmp) && RTMP_ReadPacket(rtmp, &packet))
{
if (!RTMPPacket_IsReady(&packet))
continue;
ServePacket(server, rtmp, &packet);
RTMPPacket_Free(&packet);
}
</pre>
完成握手服务后,代表服务器与客户端完成了连接,在保证连接一直存在的情况下,客户端发来请求创建连接的请求,服务端通过RTMP_ReadPacket()函数来接收客户端发过来的数据并保存在先前定义的packet中,再调用ServePacket()函数,在ServePacket()函数中,通过packet数据中的m_packetType来选择接下来的处理过程。按RTMP通讯流程,此时传递过来的命令是创建连接(Connect),而Connect命令的m_packetType为0x14 RTMP_PACKET_TYPE_INVOKE,消息Message ID为20,此消息是经过了AMF0编码的消息。
为了处理这类消息,我们定义了ServeInvoke()函数,在ServeInvoke()函数中,主要完成三项功能,即创建RTMP流媒体连接、创建流以及播放功能。
首先,RTMP传输的Message消息是经过AMF编码的,所以先调用AMF_Decode()函数解析包含命令的数据,然后根据调用AMFProp_GetString()来获取具体命令的字符串。通过AVMATCH来对比收到的命令与协议规定的命令,来完成具体的功能。例如:
AVMATCH(&method,
&av_connect)//连接
AVMATCH(&method, &av_create
Stream)//创建流
AVMATCH(&method,
&av_play)//播放
RTMP网络连接的过程如图1-7所示,具体过程如下:
(1)客户端发送“连接”(connect)消息给服务器,请求与服务器的一个实例建立连接。
(2)服务器收到连接消息后,向客户端发送询问窗口大小协议,同时连接到客户端消息提到的应用程序。
(3)同时服务器向客户端发送设置带宽协议消息。
(4)客户端设置好带宽协议消息之后,向服务器发送确认消息。
(5)服务器向客户端发送用户控制消息中的“流开始”消息。
(6)服务器发送命令消息中的“结果”给客户端,完成连接。
服务器解析客户端发送的packet包含的连接参数的内容之后,按照客户端的要求,设置好Net Connection的参数,向客户端发送。
<pre>
SetChunkSizeC1(r);
SetChunkSizeC2(r);
SetChunkSizeC3(r);
SendConnectResult(r, txn);
SendonBWDoneResult(r);
</pre>
完成RTMP网络流的流程图如1-8所示,具体过程如下:
(1)客户端发送“创建流”命令消息到服务器端。
(2)服务器收到“创建流”命令之后,发送命令消息中的“结果”(_result)消息给客户端,完成创建流。
(3)服务器收到客户端的命令后,直接调用SendResultNumber(r, txn, 1)回复。
(4)完成RTMP流播放的过程如下:
(5)客户端发送“播放”(play)命令消息给服务器。
(6)服务器接收到播放命令消息后,向客户端发送设置块大小(ChunkSize)协议命令消息。
(7)接着服务器向客户端发送“streambegin”命令消息,通知客户端流的ID。
(8)如果播放命令成功的话,服务器会接着向客户端发送“响应状态”消息,即NetStream.Play.Start和NetStream.Play.reset两个消息,告知客户端“播放”命令执行成功。
(9)接着服务器向客户端发送音频和视频数据,在我们监控系统中只需发送视频数据。
服务器收到客户端的play命令后,发送如下命令:
<pre>
SetChunkSizeP2(r);
SendReset(r);
SendStart(r);
SendRtmpSampleAccessResult(r);
</pre>
接下来我们分析最关键av_play命令,接收到此命令后,调用函数完成实时播放的实现。
由于是要保证实时传输,我们需要开辟新的线程来处理实时封装和实时传输任务,在新的线程中运行的函数为SendFlvFile(),代码如下:
<pre>
static void SendFlvFile(voidlparam)
{
RTMP r=(RTMP)lparam;
SetChunkSizeP2(r);
SendReset(r);
SendStart(r);
SendRtmpSampleAccessResult(r);
if(control.client_num<=1)
{
RTMP264_Send(&control.cam,lparam);
}
else
{
RTMP_Send_Clients(&control.cam,lparam);
}
return (void *)0;
}
</pre>
首先发送设置块大小的消息,接下来判断是否为第一个接入的客户,如果是,就启用RTMP264_Send()函数进行编码采集端开始采集编码封装发送,如果不是,则调用RTMP_Send_Clients()函数直接从共享缓存中提取数据进行发送。
在RTMP264_Send()函数中,主要实现对采集编码端获得的H264数据进行封装,RTMP发送的数据流的格式为FLV格式,因此,我们需要对包含H264数据的Nalu结构进行FLV封装。
按照FLV的格式,发送FLV数据时,首先需要设置好脚本Tag的数据。开启H264编码后,获得的第一和第二个Nalu数据就是H264数据的SPS与PPS信息,包含了初始化H.264解码器所需要的信息参数,包括编码所用的profile,level,图像的宽和高,deblock滤波器等。我们定义了metaData结构体来保存脚本Tag数据,将H264流的SPS和PPS中,保存到metaData结构体中,并将从SPS中提取到的图像width、height和帧率保存到metaData数据中。
我们的数据流只包含视频流数据,根据FLV协议的定义,需要将视频的配置信息需要写入第二个tag(metadata之后)中,在mataData数据中已经包含相关消息,通过mataData数据完成AVC视频流的配置信息的配置,通过SendVideoSpsPps()发送。
从第三个Tag开始,连续的将采集端获得的包含一副图像的Nalu封装成Tag数据,然后通过RTMP协议发送给客户端,关键代码如下:
<pre>
body[i++] = 0x17;// 1:Iframe/2:Pframe7:AVC
body[i++] = 0x01;// AVC NALU
body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;
// NALU size
body[i++] = size>>24 &0xff;
body[i++] = size>>16 &0xff;
body[i++] = size>>8 &0xff;
body[i++] = size&0xff;
// NALU data
memcpy(&body[i],data,size);
</pre>
遇到关键帧就调用SendVideoSpsPps(),重新发送AVC视频流的配置信息,然后调用SendPacket()发送视频数据。遇到P帧数据时,则直接调用SendPacket()发送视频数据。
SendPacket()主要是将FLV数据封装到RTMP的数据单元Message中,然后通过Librtmp库中的RTMP_SendPacket()函数发送到客户端。
至此,完成了视频服务器的功能设计。