前言
本文從零基礎一步步實現ONVIF協議、RTSP/RTP協議獲取IPC實時視頻流、FFMPEG解碼。開發環境為WIN7 32位 + VS2010。
最終成功獲取浩雲、海康、大華的IPC實時視頻流。
如果要了解本文更多細節,或者用本文作設計指導,那最好把文中提到的連接都打開,與本文對照着看。
前期准備
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很容易在網上可以下載。
測試ONVIF
看《ONVIF協議書》估計很多人都會雲里霧里,實在搞不懂的話,那就接上IPC,打開 Test Tool,測試一下各項功能。Test Tool里可以看到整個協議的工作細節,每一步做什么,發了什么報文,收了什么報文,都可以看到。


如果你沒有IPC,那用VLC理論上也可以,不過我沒測試過。兩個VLC(一個作為服務器一個作為客戶端)加上IP抓包工具,這個我用過也可以看到協議細節。不過從抓包工具里看到的只是一段段的報文,沒有步驟說明,不如Test tool來得明了。
當然,如果你看懂了ONVIF協議,那就不必搞這些。
Soap及開發框架生成
本人一開始並沒有聽過soap,只好自已查資料去了,這里也不班門弄斧。這個開發框架生成網上大把大把的資料,但系都不好使啊。每個人的開發環境都有一點點差別,以至於很多文章都不能從頭至尾的跟着走一次。唯有看比較多的文章再總結一下,才能自己生成一個框架。所以我這里也不多說了,推薦鏈接:
E.http://blog.csdn.net/saloon_yuan/article/details/24901597
當然也可以不用框架。如你所見,ONVIF的實現最終不過是發送報文和接收報文,用到的功能不多的話完全可以自己手動發報文過去,再解析接收到的報文。
后面也會說到不用框架來發現設備。
ONVIF設備發現、設備搜索
設備發現的過程:客戶端發送報文到239.255.255.250的3702端口(ONVIF協議規定),服務器收到報文后再向客戶端返回一個報文,客戶端收到這個報文然后解析出Xaddrs,這就完成了一次設備發現。
理論上只要報文能正確收發就可以發現設備。不過,用soap框架搜索設備的時候,多網卡、跨網段等復雜網絡會出現搜索不到的問題。這時候就需要路由支持才行(網上說可以加入相關的路由協議)。
為了解決這個問題,自己又寫了一個非SOAP框架的設備發現函數。兩個發現函數請看下面代碼。
/* 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);來釋放所有空間。這會造成內存泄漏。
/*
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為設備的端地址。要復制這段代碼的,請自行修改。
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("\nrecv retval = %d \n",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]=='\n' || http[t]=='\r'|| http[t]=='\0' ){ 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 ******\n", FoundDevNo ); printf("Device Service Address : %s\r\n", resp.wsdd__ProbeMatches->ProbeMatch->XAddrs); printf("Device EP Address : %s\r\n", resp.wsdd__ProbeMatches->ProbeMatch->wsa__EndpointReference.Address); printf("Device Type : %s\r\n", resp.wsdd__ProbeMatches->ProbeMatch->Types); printf("Device Metadata Version : %d\r\n", resp.wsdd__ProbeMatches->ProbeMatch->MetadataVersion); //sleep(1); } } else{ printf("[%d]: recv soap error :%d, %s, %s\n", __LINE__, soap->error, *soap_faultcode(soap), *soap_faultstring(soap)); } } else if (soap->error){ if (FoundDevNo){ printf("Search end! Find %d Device! \n", FoundDevNo); } else { printf("No Device found!\n"); } break; } #endif } //錯誤處理 if(retval!=1234){ this->error = NO_DEVICE ; memcpy(this->error_info_, "ONVIF: this device NOT found\n", 128); char tmp[128]; sprintf_s(tmp,128,"ONVIF: found %d devices \n",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 >> 設置搜索范圍 >> 發送報文 >> 接收並解析報文 。
非SOAP框架 設備發現
以下代碼也實現了事個發現過程,用到幾個類變量:this->IP是要發現的設備IP,this->Port是其端口號,this->LocalIP是用來保存與設備IP同一網段的本地IP地址,this->FIND_BASE_UDP_PORT是用於連接設備的本地端口號(如果此端口號被占用,會自動加2查找下一個)。
關於char *cxml 報文,最直接的方法是從Test tool里復制過來,或者從抓包工具里復制出來,當然也可以復制本文的。
流程為:輸入要發現的設備IP - 判斷本地是否添加了此IP網段 - 測試設備是否可以連接 - 綁定本地IP - 准備報文 - 發送報文 - 接收報文 - 解析報文 。
/* 獲取本機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 = "</";//"</wsdd:XAddrs>"; 