前言
本文从零基础一步步实现ONVIF协议、RTSP/RTP协议获取IPC实时视频流、FFMPEG解码。开发环境为WIN7 32位 + VS2010。
最终成功获取浩云、海康、大华的IPC实时视频流。
如果要了解本文更多细节,或者用本文作设计指导,那最好把文中提到的连接都打开,与本文对照着看。
- 1
- 2
- 3
- 4
前期准备
1.准备一个ONVIF服务器
既然开发的是客户端,那必需要有服务端了。我这里大把的IPC,好几个品牌的,就随便拿了一个。
如果没有IPC,倒是可以用 VLC media player 搭建一下。或者其他播放器也可以。这个网上很多资料。
2.准备一个ONVIF 测试工具
这个工具在ONVIF的官网上可以找到:ONVIF Device Test Tool 。
3.准备解码器相关资料及资源
收到视频流后,需要解码。可以用ffmpeg,也可以用其他解码库。这个是后话了,等ONVIF搞定之后再搞解码也不迟。推荐链接:
http://wenku.baidu.com/view/f8c94355c281e53a5802ffe4.html?re=view
(Windows下使用MinGW编译ffmpeg与x265)
4.准备资料
ONVIF协议书必看,ONVIF官网自然是不能少的。其他资料推荐几个链接:
http://www.cuplayer.com/player/PlayerCode/camera/2015/1119/2156.html
http://blog.csdn.net/gubenpeiyuan/article/details/25618177#
http://blog.csdn.net/saloon_yuan/article/details/24901597
http://www.onvif.org/onvif/ver20/util/operationIndex.html
5.准备抓包工具IPAnalyse
关系到网络通信的有个IP抓包工具能让你省去很多麻烦,也能让你清楚的看到协议的细节。IPAnalyse很容易在网上可以下载。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
测试ONVIF
看《ONVIF协议书》估计很多人都会云里雾里,实在搞不懂的话,那就接上IPC,打开 Test Tool,测试一下各项功能。Test Tool里可以看到整个协议的工作细节,每一步做什么,发了什么报文,收了什么报文,都可以看到。
- 1
- 2
如果你没有IPC,那用VLC理论上也可以,不过我没测试过。两个VLC(一个作为服务器一个作为客户端)加上IP抓包工具,这个我用过也可以看到协议细节。不过从抓包工具里看到的只是一段段的报文,没有步骤说明,不如Test tool来得明了。
当然,如果你看懂了ONVIF协议,那就不必搞这些。
- 1
- 2
- 3
Soap及开发框架生成
本人一开始并没有听过soap,只好自已查资料去了,这里也不班门弄斧。这个开发框架生成网上大把大把的资料,但系都不好使啊。每个人的开发环境都有一点点差别,以至于很多文章都不能从头至尾的跟着走一次。唯有看比较多的文章再总结一下,才能自己生成一个框架。所以我这里也不多说了,推荐链接:
- 1
- 2
E.http://blog.csdn.net/saloon_yuan/article/details/24901597
当然也可以不用框架。如你所见,ONVIF的实现最终不过是发送报文和接收报文,用到的功能不多的话完全可以自己手动发报文过去,再解析接收到的报文。
- 1
- 2
后面也会说到不用框架来发现设备。
ONVIF设备发现、设备搜索
设备发现的过程:客户端发送报文到239.255.255.250的3702端口(ONVIF协议规定),服务器收到报文后再向客户端返回一个报文,客户端收到这个报文然后解析出Xaddrs,这就完成了一次设备发现。
理论上只要报文能正确收发就可以发现设备。不过,用soap框架搜索设备的时候,多网卡、跨网段等复杂网络会出现搜索不到的问题。这时候就需要路由支持才行(网上说可以加入相关的路由协议)。
为了解决这个问题,自己又写了一个非SOAP框架的设备发现函数。两个发现函数请看下面代码。
- 1
- 2
- 3
- 4
/*
SOAP初始化
*/
int UserInitSoap(struct soap *soap,struct SOAP_ENV__Header *header)
{
if(soap == NULL){
return NULL;
}
//命名空间
soap_set_namespaces( soap, namespaces);
//参数设置
int timeout = 5 ;
soap->recv_timeout = timeout ;
soap->send_timeout = timeout ;
soap->connect_timeout = timeout ;
// 生成GUID , Linux下叫UUID
char buf[128];
GUID guid;
if(CoCreateGuid(&guid)==S_OK){
_snprintf_s(buf, sizeof(buf)
,"urn:uuid:%08X-%04X-%04x-%02X%02X-%02X%02X%02X%02X%02X%02X"
, guid.Data1 , guid.Data2 , guid.Data3
, guid.Data4[0], guid.Data4[1]
, guid.Data4[2], guid.Data4[3], guid.Data4[4],guid.Data4[5]
, guid.Data4[6], guid.Data4[7] );
}
else{
_snprintf_s(buf, sizeof(buf),"a5e4fffc-ebb3-4e9e-913e-7eecdf0b05e8");
}
//初始化
soap_default_SOAP_ENV__Header(soap, header);
//相关参数写入句柄
header->wsa__MessageID =(char *)soap_malloc(soap,128);
memset(header->wsa__MessageID, 0, 128);
memcpy(header->wsa__MessageID, buf, strlen(buf));
header->wsa__Action = "http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe";
header->wsa__To = "urn:schemas-xmlsoap-org:ws:2005:04:discovery";
//写入变量
soap->header = header;
return 0 ;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
SOAP里所有要申请的空间请用带有soap_XXX()的,否则不能通过soap_del(soap);来释放所有空间。这会造成内存泄漏。
- 1
- 2
/*
SOAP释放
*/
int UserReleaseSoap(struct soap *soap)
{
//soap_free(soap);
soap_destroy(soap);
soap_end(soap);
soap_done(soap);
soap_del(soap);
/*
The gSOAP engine uses a memory management method to allocate and deallocate memory.
The deallocation is performed with soap_destroy() followed by soap_end().
However, when you compile with -DDEBUG or -DSOAP_MEM_DEBUG then no memory
is released until soap_done() is invoked. This ensures that the gSOAP engine
can track all malloced data to verify leaks and double frees in debug mode.
Use -DSOAP_DEBUG to use the normal debugging facilities without memory debugging.
Note that some compilers have DEBUG enabled in the debug configuration, so this behavior
should be expected unless you compile in release config
*/
return 0 ;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
以下这里有用到类,其中IP为要发现设备IP,XAddrs为设备的端地址。要复制这段代码的,请自行修改。
- 1
- 2
int GetVideo::FindONVIFservers()
{
//清除错误
this->error = 0 ;
memset(this->error_info , 0, 1024);
memset(this->error_info_, 0, 1024);
int FoundDevNo = 0; //发现的设备数
int retval = SOAP_OK; //返回值
struct __wsdd__ProbeMatches resp;
struct SOAP_ENV__Header header;
struct soap* soap;
soap = soap_new();
UserInitSoap(soap,&header);
////////////////////////////////////////////
//send the message broadcast and wait
//IP Adress and PortNo, broadCast
const char *soap_endpoint = "soap.udp://239.255.255.250:3702/"; //从ONVIF协议得来
//范围相关参数
wsdd__ProbeType req;
wsdd__ScopesType sScope;
soap_default_wsdd__ScopesType(soap, &sScope);
sScope.__item = "";//"http://www.onvif.org/??"
soap_default_wsdd__ProbeType(soap, &req);
req.Scopes = &sScope;
req.Types = ""; //"dn:NetworkVideoTransmitter";
///////////////////////////////////////////////////////////////
//发送10次,直到成功为止
int time=10;
do{
retval = soap_send___wsdd__Probe(soap, soap_endpoint, NULL, &req);
Sleep(100);
}while(retval != SOAP_OK && time--);
////////////////////////////////////////////////////////////////
//一直接收,直到收到当前IP的信息后退出,或收不到当前IP但所有已接收完退出
while (retval == SOAP_OK)
{
retval = soap_recv___wsdd__ProbeMatches(soap, &resp);
//printf("
recv retval = %d
",retval);
if (retval == SOAP_OK) {
if (!soap->error){
FoundDevNo ++; //找到一个设备
if (resp.wsdd__ProbeMatches->ProbeMatch != NULL &&
resp.wsdd__ProbeMatches->ProbeMatch->XAddrs != NULL
){
//判断IP是否是你想要找的IP
char *http = strstr(resp.wsdd__ProbeMatches->ProbeMatch->XAddrs, this->IP );
if(http!=NULL){
//得到XAddrs(这里认为不超过256)
//因为有些设备有多个XAddrs,这里要分离出一个
memcpy(this->XAddrs,"http://",7);
for(int t=0;t<255-7;t++){
if(http[t]==' ' || http[t]=='
' ||
http[t]=='
'|| http[t]==' ' ){
retval = 1234 ; //退出while
break;
}
this->XAddrs[t+7] = http[t] ;
}
}//end if(http!=NULL)
}
}
else{
retval = soap->error; //退出while
this->error = soap->error ; //错误代码
const char *tmp = *soap_faultcode(soap) ; //错误信息
if(tmp)
memcpy(error_info , tmp , strlen(tmp )); //复制到类
const char *tmp_ = *soap_faultstring(soap) ;
if(tmp_)
memcpy(error_info_, tmp_, strlen(tmp_));
}
}
else if (soap->error){ //搜索完所有设备
if (FoundDevNo){
soap->error = 0 ;
retval = 0;
}
else {
retval = soap->error;
}
break; //退出while
}
#ifdef DEBUG_INFOPRINT
if (retval == SOAP_OK) {
if (!soap->error){ //找到一个设备
//打印相关信息
if (resp.wsdd__ProbeMatches->ProbeMatch != NULL && resp.wsdd__ProbeMatches->ProbeMatch->XAddrs != NULL){
printf("****** No %d Devices Information ******
", FoundDevNo );
printf("Device Service Address : %s
", resp.wsdd__ProbeMatches->ProbeMatch->XAddrs);
printf("Device EP Address : %s
", resp.wsdd__ProbeMatches->ProbeMatch->wsa__EndpointReference.Address);
printf("Device Type : %s
", resp.wsdd__ProbeMatches->ProbeMatch->Types);
printf("Device Metadata Version : %d
", resp.wsdd__ProbeMatches->ProbeMatch->MetadataVersion);
//sleep(1);
}
}
else{
printf("[%d]: recv soap error :%d, %s, %s
", __LINE__, soap->error, *soap_faultcode(soap), *soap_faultstring(soap));
}
}
else if (soap->error){
if (FoundDevNo){
printf("Search end! Find %d Device!
", FoundDevNo);
}
else {
printf("No Device found!
");
}
break;
}
#endif
}
//错误处理
if(retval!=1234){
this->error = NO_DEVICE ;
memcpy(this->error_info_, "ONVIF: this device NOT found
", 128);
char tmp[128];
sprintf_s(tmp,128,"ONVIF: found %d devices
",FoundDevNo);
memcpy(this->error_info, tmp, 128);
}
////////////////////////////////////////////////
// 退出
Sleep(10);
UserReleaseSoap(soap);
return retval;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
以上代码实现了整个设备发现的过程,最终得到设备的XAddrs。流程为:设置命名空间 >> 超时设置 >> 生成GUID >> 填充header >> 设置搜索范围 >> 发送报文 >> 接收并解析报文 。
- 1
- 2
非SOAP框架 设备发现
以下代码也实现了事个发现过程,用到几个类变量:this->IP是要发现的设备IP,this->Port是其端口号,this->LocalIP是用来保存与设备IP同一网段的本地IP地址,this->FIND_BASE_UDP_PORT是用于连接设备的本地端口号(如果此端口号被占用,会自动加2查找下一个)。
关于char *cxml 报文,最直接的方法是从Test tool里复制过来,或者从抓包工具里复制出来,当然也可以复制本文的。
流程为:输入要发现的设备IP - 判断本地是否添加了此IP网段 - 测试设备是否可以连接 - 绑定本地IP - 准备报文 - 发送报文 - 接收报文 - 解析报文 。
- 1
- 2
- 3
- 4
/*
获取本机IP
*/
bool GetLocalIPs(char ips[][32], int maxCnt, int *cnt)
{
//初始化wsa
WSADATA wsaData;
int ret=WSAStartup(0x0202,&wsaData);
if (ret!=0){ return false; }
//获取主机名
char hostname[256];
ret=gethostname(hostname,sizeof(hostname));
if (ret==SOCKET_ERROR){ return false; }
//获取主机ip
HOSTENT* host=gethostbyname(hostname);
if (host==NULL){ return false; }
//转化为char*并拷贝返回
//注意这里,如果你本地IP多于32个就可能出问题了
*cnt=host->h_length<maxCnt? host->h_length:maxCnt;
for (int i=0;i<*cnt;i++) {
in_addr* addr =(in_addr*)*host->h_addr_list;
strcpy_s(ips[i], 16, inet_ntoa(addr[i]));
}
WSACleanup();
return true;
}
/*
TCP Online Test
*/
int TcpOnlineTest(char *IP,int Port)
{
//加载库
//启动SOCKET库,版本为2.0
WSADATA wsdata;
WSAStartup(0x0202,&wsdata);
SOCKET PtcpFD=socket(AF_INET,SOCK_STREAM,0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr=inet_addr(IP); //服务器端的地址
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(Port); //服务器端的端口
//connect(PtcpFD, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); //会阻塞很久才反应过来
//设置为非阻塞模式
//这样,在connect时,才会立马跳过,不至于等太长时间
int error = -1;
int len = sizeof(int);
timeval tm;
fd_set set;
unsigned long ul = 1;
ioctlsocket(PtcpFD, FIONBIO, &ul);
int err =connect(PtcpFD, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
if(err==-1){
tm.tv_sec = 3;
tm.tv_usec = 0;
FD_ZERO(&set);
FD_SET(PtcpFD, &set);
if( select(PtcpFD+1, NULL, &set, NULL, &tm) > 0){
getsockopt(PtcpFD, SOL_SOCKET, SO_ERROR, (char *)&error, /*(socklen_t *)*/&len);
if(error == 0){ err = 0; }
else { err = -1; }
}
else { err = -1; }
}//end if(err==-1)
if(err==0){
//设置为阻塞模式
ul = 0;
ioctlsocket(PtcpFD, FIONBIO, &ul);
char buf[16]={0};
err = send(PtcpFD, buf, 16, 0); //Online Test
if(err!=16){
err = -1 ; //此IP不在线,IP或Port有误
}
else{ err = 0 ; }
}
closesocket(PtcpFD);
WSACleanup();
return err;
}
/*
连接UDP
返回: 0 - 成功
-1 - 失败
-2 - 绑定失败
*/
int UdpConnect(char *IP, int UdpPort, int *UdpFD)
{
//加载库
//启动SOCKET库,版本为2.0
WSADATA wsdata;
WSAStartup(0x0202,&wsdata);
//然后赋值
sockaddr_in serv;
serv.sin_family = AF_INET ;
serv.sin_addr.s_addr= inet_addr(IP) ; //INADDR_ANY ;
serv.sin_port = htons(UdpPort) ;
//用UDP初始化套接字
*UdpFD = socket(AF_INET,SOCK_DGRAM,0);
if(!(*UdpFD)){ return -1; }
// 设置该套接字为广播类型,
bool optval =0 ;
int res =0;
//res =setsockopt(*UdpFD,SOL_SOCKET,SO_REUSEADDR,(char FAR *)&optval,sizeof(optval));
//时限
int nNetTimeout=100;//m秒
//setsockopt(PudpFD,SOL_S0CKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));
res|= setsockopt(*UdpFD,SOL_SOCKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));
// 缓冲区
int nBuf=100*1024;//设置为xK
res|= setsockopt(*UdpFD,SOL_SOCKET,SO_SNDBUF,(const char*)&nBuf,sizeof(int));
res|= setsockopt(*UdpFD,SOL_SOCKET,SO_RCVBUF,(const char*)&nBuf,sizeof(int));
if(res){ return -1; }
// 把该套接字绑定在一个具体的地址上 !!!!!!! 注意这里 !!!!!!!
// 这是与SOAP框架不同的地方,也是之所以可以跨网段的原因!
res =bind(*UdpFD,(sockaddr *)&serv,sizeof(sockaddr_in));
if(res==-1){
int errorcode =::GetLastError();
return -2;
}
return 0 ;
}
/*
关闭UDP
*/
int UdpClose(int UdpFD)
{
closesocket(UdpFD);
WSACleanup();
return 0 ;
}
/*
UDP SEND
*/
int UdpSend(int UdpFD,char*msg,int len)
{
int ret =send(UdpFD, msg, len, 0 );
return ret ;
}
/*
UDP SEND TO
*/
int UdpSendto(int UdpFD,char *msg, int len, char *toIP, int toPort)
{
int tolen = sizeof(struct sockaddr_in);
sockaddr_in to;
to.sin_family = AF_INET ;
to.sin_addr.s_addr = inet_addr(toIP) ; //INADDR_ANY ;
to.sin_port = htons(toPort) ;
int ret =sendto(UdpFD, msg, len, 0, (const sockaddr *)&to, tolen);
return ret ;
}
/*
UDP RECV from
返回: -1 - 没有关键字段
len - nonSoap_XAddrs 空间不足,返回当前XAddrs长度
*/
int UdpRecvfrom(int UdpFD,char *fromIP,char *buf,int size)
{
//参数配置
sockaddr_in afrom ;
afrom.sin_family = AF_INET ;
afrom.sin_addr.s_addr = INADDR_BROADCAST; //inet_addr(fromIP) ; //INADDR_BROADCAST;
// afrom.sin_port = htons(fromPort) ;
//接收
int fromlength = sizeof(SOCKADDR);
memset(buf, 0, size);
int res =recvfrom(UdpFD,buf,size,0,(struct sockaddr FAR *)&afrom,(int FAR *)&fromlength);
//如果收到的不是指定的IP,将放弃
//如果你要发现所有设备,这里需要修改
if( res &&
(afrom.sin_addr.S_un.S_addr != inet_addr(fromIP))
){
//char *afIP =inet_ntoa(afrom.sin_addr); //only for debug
res = 0 ;
}
return res ;
}
/*
得到参数,解析报文
返回: 0 - 成功
-1 - 失败
>0 - nonSoap_XAddrs空间不足
*/
int GetDiscoveryParam(char *buf,int size, char *MessageID, char *nonSoap_XAddrs,int xaddr_len)
{
//规定搜索范围
const char *type = "NetworkVideoTransmitter";
const char *Scopes = "www.onvif.org";
//是否启用搜索范围
int isUse_Type = 0 ;
int isUse_Scopes = 0 ;
// type
if(isUse_Type){
char *find_type =strstr(buf, type);
if(find_type==NULL){
return -1 ; //不是这个类型的不必解析
}
}
// Scopes
if(isUse_Type){
char *find_Scopes =strstr(buf, Scopes);
if(find_Scopes==NULL){
return -1 ; //不是这个类型的不必解析
}
}
// MessageID
char *find_uuid =strstr(buf, MessageID);
if(find_uuid==NULL){
return -1 ; //MessageID不相等的,不是广播给你的,不必解析
}
// 找到XAddrs的开始和结束处
const char *wsddXAddrs_b = "XAddrs>";//"<wsdd:XAddrs>";
const char *wsddXAddrs_e = "</";