Onvif对接Rtsp实时媒体流(基于live555)

from:  http://lib.csdn.net/article/liveplay/57848

Onvif对接Rtsp实时媒体流(基于live555)

作者:yuanchunsi

Onvif(Open Network Video Interface Forum,开放型网络视频接口论坛),是安迅士联合博世及索尼公司共同成立的一个国际开放型网络视频产品标准网络接口开发论坛,以公开、开放的原则共同制定的开放型行业标准。

Onvif标准网络视频设备之间的信息交换定义通用协议,包括实时视频、音频、元数据和控制信息等。网络视频产品由此所能提供的多种可能性,使终端用户,集成商,顾问和生产厂商能够轻松地从中获益,并获得高性价比、更灵活的解决方案、市场扩张的机会以及更低的风险。

gSOAP一种跨平台的C和 C++软件开发工具包。生成C/C++的RPC代码,XML数据绑定,对SOAP Web服务和其他应用形成高效的具体架构解析器。

WSDL(网络服务描述语言)是一个用来描述Web服务和说明如何与Web服务通信的XML语言。

本软件是用gSOAP实现了onvif中定义的设备发现和Webservice服务。在使用gSOAP时首先是根据wsdl文件生成相应的头文件,然后根据头文件里申明的功能函数,在源文件中实现这些功能函数。

1生成onvif依赖文件

下载gSOAP
http://www.cs.fsu.edu/~engelen/soap.html
当前使用的是2.8.14版本。解压后,在bin目录中有需要的两个工具wsdl2h这个工具根据wsdl文件生成一个头文件,这个头文件中申明了很多功能函数,通过实现这些功能函数或者自定义函数来实现onvif功能。
生成onvif头文件
wsdl2h –o onvif.h –c –s –t ./typemap.dat
http://www.onvif.org/onvif/ver10/device/wsdl/devicemgmt.wsdl
http://www.onvif.org/onvif/ver10/event/wsdl/event.wsdl
http://www.onvif.org/onvif/ver10/display.wsdl
http://www.onvif.org/onvif/ver10/deviceio.wsdl
http://www.onvif.org/onvif/ver20/imaging/wsdl/imaging.wsdl
http://www.onvif.org/onvif/ver10/media/wsdl/media.wsdl
http://www.onvif.org/onvif/ver20/ptz/wsdl/ptz.wsdl
http://www.onvif.org/onvif/ver10/receiver.wsdl
http://www.onvif.org/onvif/ver10/recording.wsdl
http://www.onvif.org/onvif/ver10/search.wsdl
http://www.onvif.org/onvif/ver10/network/wsdl/remotediscovery.wsdl
http://www.onvif.org/onvif/ver10/replay.wsdl
http://www.onvif.org/onvif/ver20/analytics/wsdl/analytics.wsdl
http://www.onvif.org/onvif/ver10/analyticsdevice.wsdl
http://www.onvif.org/onvif/ver10/schema/onvif.xsd
http://www.onvif.org/ver10/actionengine.wsdl
 
生成框架
soapcpp2-2 onvif.h -x -I./gsoap-2.8/gsoap/import -I./gsoap-2.8/gsoap
将wsdd.nsmap改名为wsdd.h,删除其它所有nsmap文件,它们的内容是一样的,删除soapClientLib.c,soapServerLib.c这两个文件。
从gsoap里拷贝所需文件到当前目录,这些文件包括dom.c、soapC.c、stdsoap2.c、md5evp.c、smdevp.c、mecevp.c、threads.c、wsaapi.c、wsseapi.c、duration.c
在生成框架时,2.8.14版本的gSOAP会报错,只需要注释掉./third-party/gsoap-2.8/gsoap/import/wsa5.h 278行函数的申明即可。
 
生成源文件

在上面生成的onvif.h里申明了很多函数,需要在源文件中实现这些函数。

2 ONVIF

目录及概述

1)global_init:初始化网络、内存、onvif服务、webserver及相关全局变量
2)webserver_thread: 解析soap协议并提供相应web服务
3)hello_thread: 以一定的时间间隔发送“HELLO”消息
4)probe_thread:是用来应答客户端发送的“Probe“消息
 
设备发现功能
过程描述:
1)       Onvif设备在启动后,组播“HELLO”消息,告知设备已经上线。
2)       onvif客户端监听组播消息,当收到“HELLO”消息后,单播“PROBE”消息到onvif设备端。
3)       设备端收到“PROBE”消息后检查消息中设备类型字段是否匹配,如果匹配则单播“PROBEMATCH”消息到客户端,在这个消息中包含设备端的web服务地址。
实现函数:
a) intsoap_send___wsdd__Hello(struct soap *soap, constchar*soap_endpoint, constchar*soap_action, structwsdd__HelloType*wsdd__Hello);
这个函数将由Hello线程直接调用,用来序列化“Hello“消息,然后组播。
b)int__wsdd__Probe(struct soap* soap, structwsdd__ProbeType*wsdd__Probe)
这个函数用来响应客户端的“Probe”消息
 
 
RTSP视频对接
2.3.1流程描述
1)设备发现成功后,客户端发送获取设备端能力集等请求
2)客户端发送获取音视频源信息及音视频编解码信息
3)客户端发送获取流媒体URL信息
实现函数
GetCapabilities
int__tds__GetCapabilities(struct soap* soap, struct _tds__GetCapabilities*tds__GetCapabilities, struct _tds__GetCapabilitiesResponse*tds__GetCapabilitiesResponse)
客户端通过webservice获取设备支持的功能时,webservice会调用这个函数,这个函数会告知客户端这个设备是否支持image、video、audio等,在GetCapabilities函数中我们想要对接RTSP视频,必须设置Media 和一些必要参数
 
GetServices
int__tds__GetServices(struct soap* soap, struct _tds__GetServices*tds__GetServices, struct _tds__GetServicesResponse*tds__ GetServicesResponse)
 
GetVideoSources
int__tds__GetVideoSources(struct soap* soap, struct _tds__ GetVideoSources *tds__ GetVideoSouces, struct _tds__GetVideoSourcesResponse*tds__ GetVideoSourcesResponse)
 
GetProfiles
 int__tds__GetProfiles(struct soap* soap, struct _tds__ GetProfiles *tds__ GetProfiles, struct _tds__GetProfilesResponse*tds__ GetProfilesResponse)
除了基本信息,还需要填充两大项VideoSourceConfiguration和VideoEncoderConfiguration,一个用于描述视频源的信息,另外一个描述视频的编码信息
 
GetVideoSourceConfiguration
int__tds__GetVideoSourceConfiguration(struct soap* soap, struct _tds__ GetVideoSourceConfiguration *tds__ GetVideoSourceConfiguration, struct _tds__GeVideoSourceConfigurationResponse*tds__ GeVideoSourceConfigurationResponse)
 
GetVideoEncoderConfigurationOptions
int __tds__GetVideoEncoderConfiguration(struct soap* soap, struct _tds__ GetVideoEncoderConfiguration *tds__ GetVideEncoderConfiguration, struct _tds__GetVideoEncoderConfigurationResponse*tds__ GetVideoEncoderConfigurationResponse)
 
GetStreamUri
int__trt__GetStreamUri(struct soap* soap, struct _trt__GetStreamUri*trt__GetStreamUri, struct _trt__GetStreamUriResponse*trt__GetStreamUriResponse)
当客户端通过GetCapabilities请求获知设备支持流媒体是,便会发起GetStreamUri请求,webservice返回当前流媒体的地址,这样,客户端便能播放实时流媒体。
GetSnapshotUri
int__trt__GetSnapshotUri(struct soap* soap, struct _trt__GetSnapshotUri*trt__GetSnapshotUri, struct _trt__GetSnapshotUriResponse*trt__GetSnapshotUriResponse)
 
 

编译脚本

(略......)

第三方依赖库

日志管理
为了便于调试和维护,在这个程序中增加了日志系统,包括时间戳、日志等级、所在文件、所在函数、行数以及日志内容。
使用方法:
# exportPRINT_LOG_LEVEL=6

然后运行onvif程序,便会在终端输出日志

内存管理
在这个onvif程序中,经常从系统heap中分配空间,使用之后,经常忘记释放。这个功能便是记录调用malloc分配空间的地方。
void ycs_init_memory();
初始化内存记录链表。
void* ycs_malloc(int size, constchar*file, constchar*func, constint line);
这个函数将传入的文件名、函数名、行号和分配大小记录到链表,然后返回内存地址。
void ycs_free(void*ptr, constchar*file, constchar*func, constint line);
这个函数根据ptr的值在链表中查找对应分配的空间,然后释放该空间。
void ycs_print_meminfo();
这个函数将输出链表中所有malloc分配空间的信息。

3效果图
 
 

 

1 live555

1.1 工作模块
UsageEnvironment  
该类库是对系统环境的抽象,包括UsageEnvironment 和TaskScheduler。UsageEnvironment  主要用于消息的输入输出和用户功能,TaskScheduler实现事件的异步处理,事件处理函数的注册等。它通过维护一个异步读取源实现如消息到达等事件的处理,通过使用DelayQueue实现其他注册函数的延时调度。另外,还有一个HashTable类定义了一个通用的hash表,其它代码要用 到这个表。我们在使用时可以自定义该类的抽象类的子类,就可以再特定的环境下运行如嵌入式或者GUI等。不需要进行太多的修改。
groupsock
该类是对网络接口的封装,用于收发数据包。groupsock主要是面向多播数据的收发,它也同时支持单播数据的接收。
liveMedia
该类是live555的核心模块,各种媒体的封装和数据的发送。其中基类为Medium,其他的类都派生自该类。如MediaSession,RTP会话类,一个 session又可以包含多个subsession。还有比较重要的两个派生类Source和Sink,Source抽象了需要发送的数据,Sink则抽象数据的发送者,数据的流动可以经过多个source和sink,两者又通过session联系在一起。我们在开发的过程中,可以通过继承这些类,实现自己需要的相关功能。
BasicUsageEnvironment
该类主要针对简单控制台的应用程序,利用select实现事件的获取和处理。
1.2 工作流程
 live555首先会创建一个RTSP服务(具体的实现可以参看mediaServer里的服务)。在服务创建过程中,会先调用setUpOurSocket建立tcp的连接,并监听对应传入的port,用于等待client请求的rtsp协议的交互,然后会把连接处理句柄已经socket句柄都传入TaskScheduler当中,等待事件触发。

 
                                                  (流程图)
1、初始化
BasicTaskschedular
BasicUsageEnvironment
RTSPServer
|--------new RTSPServer
|------setupOurSocket创建监听客户端连接用的socket
|------turnOnBackgroundhandling(socket)将监听socket加入计划任务,等待连接
2、接受客户端连接 incomingConnectionHandlerRTSP
在RTSPServer将监听socket加入计划任务后,调度机制会不断查询,如果收到连接请求,就会调用该回掉函数
(Tips:在Live555中监听任务的执行都是通过相应的静态全局回掉函数,在回掉函数内部再强制转换到相应的类函数)
在incomingConnectionHandlerRTSP中会clientSocket = accept()得到连接的socket、地址,设置参数等
之后创建一个客户连接管理createNewClientConnection()
|----new RTSPClientConnection
|---setBackgroundHandling将连接的socket加入计划任务
(Tips:turnonBackgroundHandling只让socket可读,内部会调用setBackgroundHandling进行设置,如需其他属性,如可写、异常等都需要通过setBackgroundHandling设置)
3、响应请求过程
3.1 一般过程
incomingRequestHandler
|---readSocket() 读取数据
|---handleRequestBytes() 解析数据并做相应的处理
|---parseRTSPRequestString() 解析请求
|---handleCmd_XXX() 根据不同的RTSP协议命令去处理
|---send(fResponseBuffer) 构造好回应字段后,发送回应
3.2具体过程
3.2.1 handleCmd_DESCRIBE
handleCmd_DESCRIBE
|---urlTotalSuffix 提取streamName
|---authenticationOK 验证用户,这里只保留了接口,未进行实现
|---fOurServer.lookupServerMediaSession(streamName)
这个用来获取ServerMediaSession,一般不同类型的服务器会有不同的实现策略,如Live555MediaServer只用来流化本地文件,所以有了继承类DynamicRTSPServer,重写lookupServerMediaSession方法,本来找不到session实例会返回NULL,而在这里将查找当前目录下对应的本地文件,然后去创建相应的session,这也就是为什么要将文件与程序放在同一目录下的原因。
这里的ServerMediaSession在创建时会同时添加ServerMediasubsession,因为是服务器,所以一定知道自己应该创建些什么subsession
|---sdpDescription =session->generateSDPDescription() 获取SDP描述信息
|---snprintf() 组建回复字符串
3.2.1.1 generateSDPDescription过程
generateSDPDescription
|---foreach subsession sdpLines =subsession->sdpLines() 获取每个subsessiond的sdp描述
|---sdpLines() 过程
|---onDemandServerMediasubsession 为点播式流媒体服务创建的中间继承类,Live555Mediasever中的subsession都继承自此类
|---FrameSource createNewStreamSource 创建一个数据源的Souce
|---RTPSink createNewRTPSink 创建RTPSink
|---setSDPLinesFromRTPSink(source, sink)从临时的source与sink中获取sdp
(Tips:createNewStreamSource与createNewRTPSink都是抽象方法,需要子类去实现,之后会从setSDPLinesFromRTPSink(source, sink)中得到sdp信息,从这里也可以看出sdp信息由RTPSink获得。Live555服务器的机制是sink从source中获取数据,创建sink的时候会将source作为参数传入,因为服务器知道创建了什么类型的subSession,所以对一般的媒体信息sink都会具备,唯一需要获取的是getAuxSDPLine(),OnDemandServerMediasubsession默认的处理方式是从RTPSink中的auxSDPLine获取,如果没有就返回NULL,对Live555MediaServer来说,传输H264文件时是无法得到sps、pps信息的,必须通过读取文件,所以在H264subsession中就选择创建Source和Sink读取一段信息后解析获得)
3.2.2 handleCmd_SETUP
handleCmd_SETUP
|---sessionID 找一个唯一的sessionID,用于标识当前的subsession
|---fourServer.createNewClientSession(sessionID)
|---clientSession.handleCmd_SETUP() 转到RTSPClientSession中去处理
|---fOurServerMediaSubsession = sms =fourSrever.lookupServerMediasession
|---创建streamState结构
|---fStreamStates = new struct streamState[fNumStreamStates]
|---foreach subSessionfstreamState[i].subsession = subsession 将subsession装进streamState结构
|---trackID,subsession->trackID 通过trackID寻找对应的subSession
|---parseTransportHeader 解析Transport参数
|---subsession->getStreamParameters 在这里将建立真正的FrameSource与RTPSink
|---FrameSource *mediaSource = createNewStreamSource()
|---rtoGroupsock = new GroupSock(serverRTPPort)
|---rtcpGroupsock = new Groupsock(serverRTPPort)
|---rtpSink = creatNewRTPSink(rtpGroupsock, mediaSource)
|---streamToken = new StreamState(rtpSink, mediaSource, rtpsock,rtcpsock)
|---fDestinationHashTable->add(sessionId, destination)
3.2.3handleCmd_PLAY
handleCmd_PLAY
|---在RTSPClientSession中,处理PLAY、PAUSE、TEARDOWN用同一个函数接口handle_CmdwithinSession,只是在里面又进行了区分
|---找到对应的subsession->startStream()这里将启动流传输数据
|---streamState.startPlaying() 开始传输
|---如果没有fRTCPInstance就创建一个RTCPInstance::createNew(fRTPSink)
|---fRTPgs->addDestination()
|---fRTCPgs->addDestination()
|---fRTPSink->startPlaying()
|---MediaSink::continuePlay() 到这里就是各个子类实现了,纯虚函数
|---MuliFramedRTPSink::continuePlaying()这里针对的是H264文件的解析
|---buildAndSendPacket()
|---准备RTP包头
|---packFrame 打包帧数据
|---分两种情况:一是上一次没打包完,还有数据;二是一个全新的帧
|---对情况二,会调用fSource->getNextFrame()里面加入的回调函数afterGettingFrame,实际是调用sink->afterGettingFrame1
|---最终会依情况打包
|---sendPacketIfNecessary()
在sendPacketIfNecessary中会调用fRTPInterface.sendPacket()发送数据,之后安排一个延时任务回调sendNext函数,在sendNext中又会调用buildAndSendPacket,从而形成一个回路来不断发送数据,知道检测到fNoFrameLeft

1.3 live555调试方法

1)调节socket发送缓存
 (略......)
2)调节live555缓存阀值
(略......)
3)更换协议TCPorUDP
(略......)
4)添加打印日志

(略......)

1.4 安卓平台编译
1、编译脚本
(略......)
2、编译指令:
a)  ./genMakefiles android 
b)  make


2 流媒体服务器
本流媒体服务器是基于live555进行的二次开发,作用是实现设备端实时音视频数据的RTSP转发。通过封装jni接口启动流媒体服务器,获取音视频实时数据进行编码、解析后,分包由live555发送。

2.1 目录结构
 


 
2.2 编译脚本
 (略......)
2.3 jni封装


功能:启动RtspServer
Java_com_eques_device_ui_hardware_RTSPJNI_RtspServer(JNIEnv*env,jobject thiz)
参数名    数据类型    描述
        
功能:取实时视频数据
Java_com_eques_device_ui_hardware_RTSPJNI_ReadVideoData(JNIEnv*env,jobject thiz,jbyteArray DataIn,jint  insize)
参数名    数据类型    描述
DataIn    jbyteArray    H264数据
insize    jint      长度


功能:取实时音频数据
Java_com_eques_device_ui_hardware_RTSPJNI_ReadAudioData(JNIEnv*env,jobject thiz,jbyteArray DataIn,jint  insize)
参数名    数据类型    描述
DataIn    jbyteArray    PCM数据
insize    jint      长度

2.4 H264实时流传输
LIVE555默认只支持发送音视频文件,而不支持从媒体设备获取的实时码流。这需要修改LIVE555源代码以实现H264码流实时发送功能。
从实现RTSP服务的相关基类派生出H264码流直播的类,重写类的成员方法来实现。。具体实现方法是添加H264LiveVideoServerMediaSubssion:public H264VideoFileServerMediaSubsession类,并重写createNewStreamSource成员方法。该成员方法的关键段代码段如下:

(略......)


该代码段的主要工作是把ByteStreamFileSource替换为用户自定义的H264FramedLiveSource,用于获取高清摄像头上的实时视频数据。H264FramedLiveSource的成员方法H264FramedLiveSource::doGetNextFrame就实现了从H264编码输出端获取H264格式视频数据并送到H264orH265VideoRTPSink端的过程。该成员方法的关键代码段如下:

(略......)


这样,当服务器端收到客户端PLAY命令时,不断调用H264FramedLiveSource::doGetNextFrame读取H264格式视频数据,封包和发送出去,实现H264码流实时传输功能。
在live555android.cpp主函数中,只需在创建ServerMediaSession时加入H264LiveVideoServerMediaSubssion,并向RTSPServer中注册该ServerMediaSession即可。

2.5 AAC实时流传输
1、AAC编码
jni接口获取的数据为pcm流,需要编码aac流
需要注意的是pcm到aac编码前需要做位的存储转化代码如下:

(略......)

 

2、重写类的成员方法来实现,具体实现方法是添加AudioLiveVideoServerMediaSubssion:public AudioVideoFileServerMediaSubsession类,并重写createNewStreamSource成员方法。该成员方法的关键段代码段如下:
 

 (略......)



AudioFramedLiveSource,用于获取高清摄像头上的实时视频数据。AudioFramedLiveSource的成员方法AudioFramedLiveSource::doGetNextFrame就实现了AAC数据的解析分包拷贝到live555缓存,关键代码段如下:

(略......)


这样,当服务器端收到客户端PLAY命令时,不断调用AudioFramedLiveSource::doGetNextFrame读取AAC格式数据,封包和发送出去,实现AAC码流实时传输功能。
在live555android.cpp主函数中,只需在创建ServerMediaSession时加入AACLiveVideoServerMediaSubssion,并向RTSPServer中注册该ServerMediaSession即可。

猜你喜欢

转载自blog.csdn.net/twd_1991/article/details/81227228