一、前言
- CMU(Center Manager Unit),即中心管理单位
- PU(Prefocus Unit),即监控前端单元,负责在CMU的控制下使用摄像机采集视频流、使用麦克风采集音频流、使用控制口采集报警信息、对摄像机云台镜头进行控制
- CU(Client Unit),监控系统的监控客户端单元,负责将PU采集到的视频流、音频流、报警信息提交给监控用户,并根据用户要求操作PU设备,如云台、镜头等
二、传统视频监控系统的一个局域网应用场景
- PU设备上线后,向CMU注册,建立连接
- CMU与PU进行信令交互,请求能力集,获取配置
- CU上线,向CMU注册,建立连接
- CMU与CU进行信令交互,传输设备列表
- CU向PU请求码流
三、应用onvif规范后场景
- PU设备上线后,向CMU发送HELLO消息
- CMU需要搜寻设备时,向PU发送PROBE消息
- CMU与PU进行信令交互,请求能力集,获取配置
- CU上线,向CMU注册,建立连接
- CMU与CU进行信令交互,传输设备列表
- 在CMU的协调下,CU同PU建立连接传输码流
有了onvif之后
- PU与CMU的交互方式发生了改变,CMU不再与PU保持长连接
- 遵循ONVIF规范,信令以及消息内容有了统一的标准
四、设备发现main函数说明
#include "soapH.h"
#include "soapStub.h"
#include "wsaapi.h"
#include "wsdd.nsmap"
#include "wsseapi.h"
#include <assert.h>
#define SOAP_ASSERT assert
#define SOAP_DBGLOG printf
#define SOAP_DBGERR printf
#define SOAP_TO "urn:schemas-xmlsoap-org:ws:2005:04:discovery"
#define SOAP_ACTION "http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe"
#define SOAP_MCAST_ADDR "soap.udp://239.255.255.250:3702" // onvif规定的组播地址
#define SOAP_ITEM "" // 寻找的设备范围
#define SOAP_TYPES "dn:NetworkVideoTransmitter" // 寻找的设备类型
#define SOAP_SOCK_TIMEOUT (10) // socket超时时间(单秒秒)
#define nullptr NULL
#define bool int
#define true 1
#define false 0
void soap_perror(struct soap *soap, const char *str)
{
if (nullptr == str) {
SOAP_DBGERR("[soap] error: %d, %s, %s\n", soap->error, *soap_faultcode(soap), *soap_faultstring(soap));
} else {
SOAP_DBGERR("[soap] %s error: %d, %s, %s\n", str, soap->error, *soap_faultcode(soap), *soap_faultstring(soap));
}
}
void *ONVIF_soap_malloc(struct soap *soap, unsigned int n)
{
void *p = nullptr;
if (n > 0) {
p = soap_malloc(soap, n);
SOAP_ASSERT(nullptr != p);
memset(p, 0x00, n);
}
return p;
}
struct soap *ONVIF_soap_new(int timeout)
{
struct soap *soap = nullptr; // soap环境变量
SOAP_ASSERT(nullptr != (soap = soap_new()));
soap_set_namespaces(soap, namespaces); // 设置soap的namespaces
soap->recv_timeout = timeout; // 设置超时(超过指定时间没有数据就退出)
soap->send_timeout = timeout;
soap->connect_timeout = timeout;
#if defined(__linux__) || defined(__linux) // 参考https://www.genivia.com/dev.html#client-c的修改:
soap->socket_flags = MSG_NOSIGNAL; // To prevent connection reset errors
#endif
soap_set_mode(soap, SOAP_C_UTFSTRING); // 设置为UTF-8编码,否则叠加中文OSD会乱码
return soap;
}
void ONVIF_soap_delete(struct soap *soap)
{
soap_destroy(soap); // remove deserialized class instances (C++ only)
soap_end(soap); // Clean up deserialized data (except class instances) and temporary data
soap_done(soap); // Reset, close communications, and remove callbacks
soap_free(soap); // Reset and deallocate the context created with soap_new or soap_copy
}
/************************************************************************
**函数:ONVIF_init_header
**功能:初始化soap描述消息头
**参数:
[in] soap - soap环境变量
**返回:无
**备注:
1). 在本函数内部通过ONVIF_soap_malloc分配的内存,将在ONVIF_soap_delete中被释放
************************************************************************/
void ONVIF_init_header(struct soap *soap)
{
struct SOAP_ENV__Header *header = nullptr;
SOAP_ASSERT(nullptr != soap);
header = (struct SOAP_ENV__Header *)ONVIF_soap_malloc(soap, sizeof(struct SOAP_ENV__Header));
soap_default_SOAP_ENV__Header(soap, header);
header->wsa__MessageID = (char *)soap_wsa_rand_uuid(soap);
header->wsa__To = (char *)ONVIF_soap_malloc(soap, strlen(SOAP_TO) + 1);
header->wsa__Action = (char *)ONVIF_soap_malloc(soap, strlen(SOAP_ACTION) + 1);
strcpy(header->wsa__To, SOAP_TO);
strcpy(header->wsa__Action, SOAP_ACTION);
soap->header = header;
}
/************************************************************************
**函数:ONVIF_init_ProbeType
**功能:初始化探测设备的范围和类型
**参数:
[in] soap - soap环境变量
[out] probe - 填充要探测的设备范围和类型
**返回:
0表明探测到,非0表明未探测到
**备注:
1). 在本函数内部通过ONVIF_soap_malloc分配的内存,将在ONVIF_soap_delete中被释放
************************************************************************/
void ONVIF_init_ProbeType(struct soap *soap, struct wsdd__ProbeType *probe)
{
struct wsdd__ScopesType *scope = nullptr; // 用于描述查找哪类的Web服务
SOAP_ASSERT(nullptr != soap);
SOAP_ASSERT(nullptr != probe);
scope = (struct wsdd__ScopesType *)ONVIF_soap_malloc(soap, sizeof(struct wsdd__ScopesType));
soap_default_wsdd__ScopesType(soap, scope); // 设置寻找设备的范围
scope->__item = (char *)ONVIF_soap_malloc(soap, strlen(SOAP_ITEM) + 1);
strcpy(scope->__item, SOAP_ITEM);
memset(probe, 0x00, sizeof(struct wsdd__ProbeType));
soap_default_wsdd__ProbeType(soap, probe);
probe->Scopes = scope;
probe->Types = (char *)ONVIF_soap_malloc(soap, strlen(SOAP_TYPES) + 1); // 设置寻找设备的类型
strcpy(probe->Types, SOAP_TYPES);
}
void ONVIF_DetectDevice(void (*cb)(char *DeviceXAddr))
{
int i;
int result = 0;
unsigned int count = 0; // 搜索到的设备个数
struct soap *soap = nullptr; // soap环境变量
struct wsdd__ProbeType req; // 用于发送Probe消息
struct __wsdd__ProbeMatches rep; // 用于接收Probe应答
struct wsdd__ProbeMatchType *probeMatch;
SOAP_ASSERT(nullptr != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));
ONVIF_init_header(soap); // 设置消息头描述
ONVIF_init_ProbeType(soap, &req); // 设置寻找的设备的范围和类型
result = soap_send___wsdd__Probe(soap, SOAP_MCAST_ADDR, nullptr, &req); // 向组播地址广播Probe消息
while (SOAP_OK == result) // 开始循环接收设备发送过来的消息
{
memset(&rep, 0x00, sizeof(rep));
result = soap_recv___wsdd__ProbeMatches(soap, &rep);
if (SOAP_OK == result) {
if (soap->error) {
soap_perror(soap, "ProbeMatches");
} else { // 成功接收到设备的应答消息
// printf("__sizeProbeMatch:%d\n",rep.wsdd__ProbeMatches->__sizeProbeMatch);
if (nullptr != rep.wsdd__ProbeMatches) {
count += rep.wsdd__ProbeMatches->__sizeProbeMatch;
for (i = 0; i < rep.wsdd__ProbeMatches->__sizeProbeMatch; i++) {
probeMatch = rep.wsdd__ProbeMatches->ProbeMatch + i;
if (nullptr != cb) {
char *url = "http://192.168.0.116/onvif/device_service";
if (memcpy(probeMatch->XAddrs, url, strlen(url)) == 0) {
cb(probeMatch->XAddrs); // 使用设备服务地址执行函数回调
}
}
}
}
}
} else if (soap->error) {
break;
}
}
SOAP_DBGLOG("\ndetect end! It has detected %d devices!\n", count);
if (nullptr != soap) {
ONVIF_soap_delete(soap);
}
}
#define SOAP_CHECK_ERROR(result, soap, str) \
do { \
if (SOAP_OK != (result) || SOAP_OK != (soap)->error) { \
soap_perror((soap), (str)); \
if (SOAP_OK == (result)) { \
(result) = (soap)->error; \
} \
goto EXIT; \
} \
} while (0)
/************************************************************************
**函数:ONVIF_SetAuthInfo
**功能:设置认证信息
**参数:
[in] soap - soap环境变量
[in] username - 用户名
[in] password - 密码
**返回:
0表明成功,非0表明失败
**备注:
************************************************************************/
static int ONVIF_SetAuthInfo(struct soap *soap, const char *username, const char *password)
{
int result = 0;
SOAP_ASSERT(nullptr != username);
SOAP_ASSERT(nullptr != password);
result = soap_wsse_add_UsernameTokenDigest(soap, NULL, username, password);
SOAP_CHECK_ERROR(result, soap, "add_UsernameTokenDigest");
EXIT:
return result;
}
#define USERNAME "admin"
#define PASSWORD "hik12345"
/************************************************************************
**函数:ONVIF_GetDeviceInformation
**功能:获取设备基本信息
**参数:
[in] DeviceXAddr - 设备服务地址
**返回:
0表明成功,非0表明失败
**备注:
************************************************************************/
int ONVIF_GetDeviceInformation(const char *DeviceXAddr)
{
int result = 0;
struct soap *soap = nullptr;
struct _tds__GetDeviceInformation devinfo_req;
struct _tds__GetDeviceInformationResponse devinfo_resp;
SOAP_ASSERT(nullptr != DeviceXAddr);
SOAP_ASSERT(nullptr != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));
ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);
result = soap_call___tds__GetDeviceInformation(soap, DeviceXAddr, nullptr, &devinfo_req, &devinfo_resp);
SOAP_CHECK_ERROR(result, soap, "GetDeviceInformation");
// std::cout << " Manufacturer:\t" << devinfo_resp.Manufacturer << "\n";
// std::cout << " Model:\t" << devinfo_resp.Model << "\n";
// std::cout << " FirmwareVersion:\t" << devinfo_resp.FirmwareVersion << "\n";
// std::cout << " SerialNumber:\t" << devinfo_resp.SerialNumber << "\n";
// std::cout << " HardwareId:\t" << devinfo_resp.HardwareId << "\n";
EXIT:
if (nullptr != soap) {
ONVIF_soap_delete(soap);
}
return result;
}
/************************************************************************
**函数:ONVIF_GetSnapshotUri
**功能:获取设备图像抓拍地址(HTTP)
**参数:
[in] MediaXAddr - 媒体服务地址
[in] ProfileToken - the media profile token
[out] uri - 返回的地址
[in] sizeuri - 地址缓存大小
**返回:
0表明成功,非0表明失败
**备注:
1). 并非所有的ProfileToken都支持图像抓拍地址。举例:XXX品牌的IPC有如下三个配置profile0/profile1/TestMediaProfile,其中TestMediaProfile返回的图像抓拍地址就是空指针。
************************************************************************/
int ONVIF_GetSnapshotUri(const char *MediaXAddr, const char *ProfileToken, char **snapUri)
{
int result = 0;
struct soap *soap = nullptr;
struct _trt__GetSnapshotUri req;
struct _trt__GetSnapshotUriResponse rep;
SOAP_ASSERT(MediaXAddr != NULL && ProfileToken != NULL);
SOAP_ASSERT(nullptr != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));
ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);
req.ProfileToken = (char *)ProfileToken;
result = soap_call___trt__GetSnapshotUri(soap, MediaXAddr, NULL, &req, &rep);
SOAP_CHECK_ERROR(result, soap, "GetSnapshotUri");
if (nullptr != rep.MediaUri && nullptr != rep.MediaUri->Uri) {
*snapUri = rep.MediaUri->Uri;
}
EXIT:
if (NULL != soap) {
ONVIF_soap_delete(soap);
}
return result;
}
bool ONVIF_GetProfiles(const char *mediaXAddr, char **profilesToken)
{
int result = 0;
struct soap *soap = nullptr;
struct _trt__GetProfiles devinfo_req;
struct _trt__GetProfilesResponse devinfo_resp;
SOAP_ASSERT(mediaXAddr != NULL);
SOAP_ASSERT(nullptr != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));
ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);
result = soap_call___trt__GetProfiles(soap, mediaXAddr, nullptr, &devinfo_req, &devinfo_resp);
SOAP_CHECK_ERROR(result, soap, "ONVIF_GetProfiles");
SOAP_ASSERT(devinfo_resp.__sizeProfiles > 0);
*profilesToken = devinfo_resp.Profiles->token;
EXIT:
if (nullptr != soap) {
ONVIF_soap_delete(soap);
}
return result;
}
/************************************************************************
**函数:ONVIF_GetCapabilities
**功能:获取设备能力信息
**参数:
[in] DeviceXAddr - 设备服务地址
[in]
**返回:
0表明成功,非0表明失败
**备注:
1). 其中最主要的参数之一是媒体服务地址
************************************************************************/
int ONVIF_GetCapabilities(const char *deviceXAddr, char **mediaXAddr)
{
int result = 0;
struct soap *soap = nullptr;
struct _tds__GetCapabilities devinfo_req;
struct _tds__GetCapabilitiesResponse devinfo_resp;
SOAP_ASSERT(deviceXAddr != NULL);
SOAP_ASSERT(nullptr != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));
result = soap_call___tds__GetCapabilities(soap, deviceXAddr, NULL, &devinfo_req, &devinfo_resp);
SOAP_CHECK_ERROR(result, soap, "GetCapabilities");
if (devinfo_resp.Capabilities->Media != nullptr) {
*mediaXAddr = devinfo_resp.Capabilities->Media->XAddr;
}
EXIT:
if (nullptr != soap) {
ONVIF_soap_delete(soap);
}
return result;
}
/************************************************************************
**函数:make_uri_withauth
**功能:构造带有认证信息的URI地址
**参数:
[in] src_uri - 未带认证信息的URI地址
[in] username - 用户名
[in] password - 密码
[out] dest_uri - 返回的带认证信息的URI地址
[in] size_dest_uri - dest_uri缓存大小
**返回:
0成功,非0失败
**备注:
1). 例子:
无认证信息的uri:rtsp://100.100.100.140:554/av0_0
带认证信息的uri:rtsp://username:[email protected]:554/av0_0
************************************************************************/
static int make_uri_withauth(const char *src_uri, const char *username, char *password, char **dest_uri)
{
int result = 0;
SOAP_ASSERT(src_uri != NULL);
if (username == NULL && password == NULL) { // 生成新的uri地址
*dest_uri = src_uri;
} else {
/*
std::string::size_type position = src_uri.find("//");
if (std::string::npos == position) {
SOAP_DBGERR("can't found '//', src uri is: %s.\n", src_uri.c_str());
result = -1;
return result;
}
position += 2;
dest_uri->append(src_uri,0, position) ;
dest_uri->append(username + ":" + password + "@");
dest_uri->append(src_uri,position, std::string::npos) ;
*/
}
return result;
}
/************************************************************************
**函数:ONVIF_GetStreamUri
**功能:获取设备码流地址(RTSP)
**参数:
[in] MediaXAddr - 媒体服务地址
[in] ProfileToken - the media profile token
**返回:
0表明成功,非0表明失败
**备注:
************************************************************************/
int ONVIF_GetStreamUri(const char *MediaXAddr, const char *ProfileToken)
{
int result = 0;
struct soap *soap = nullptr;
struct tt__StreamSetup ttStreamSetup;
struct tt__Transport ttTransport;
struct _trt__GetStreamUri req;
struct _trt__GetStreamUriResponse rep;
SOAP_ASSERT(MediaXAddr != NULL);
SOAP_ASSERT(nullptr != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));
ttStreamSetup.Stream = tt__StreamType__RTP_Unicast;
ttStreamSetup.Transport = &ttTransport;
ttStreamSetup.Transport->Protocol = tt__TransportProtocol__RTSP;
ttStreamSetup.Transport->Tunnel = nullptr;
req.StreamSetup = &ttStreamSetup;
req.ProfileToken = (char *)(ProfileToken);
ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);
result = soap_call___trt__GetStreamUri(soap, MediaXAddr, nullptr, &req, &rep);
SOAP_CHECK_ERROR(result, soap, "GetServices");
if (nullptr != rep.MediaUri) {
if (nullptr != rep.MediaUri->Uri) {
// std::cout << rep.MediaUri->Uri << "\n";
printf("Url:%s\n", rep.MediaUri->Uri);
}
}
EXIT:
if (nullptr != soap) {
ONVIF_soap_delete(soap);
}
return result;
}
void cb_discovery(char *deviceXAddr)
{
char *mediaXAddr, profilesToken, snapUri, snapAuthUri;
ONVIF_GetCapabilities(deviceXAddr, &mediaXAddr);
ONVIF_GetProfiles(mediaXAddr, &profilesToken);
ONVIF_GetStreamUri(mediaXAddr, profilesToken);
/* ONVIF_GetSnapshotUri(mediaXAddr, profilesToken, &snapUri);
make_uri_withauth(snapUri, USERNAME, PASSWORD, &snapAuthUri);
char cmd[256];
sprintf(cmd, "wget -O %s '%s'", "out.jpeg", snapAuthUri.c_str()); // 使用wget下载图片
system(cmd);*/
}
int main(int argc, char **argv)
{
ONVIF_DetectDevice(cb_discovery);
return 0;
}
五、CMakeLists.txt说明
- soap目录编译成了libonvif_soap,依赖openssl,还需要定义两个宏WITH_OPENSSL和 WITH_DOM
- onvif目录编译成了libonvif,依赖libonvif_soap.
cmake_minimum_required(VERSION 3.5)
project(ONVIF-DEMO)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DWITH_NONAMESPACES -DWITH_DOM -DWITH_OPENSSL")
include_directories(${PROJECT_SOURCE_DIR}/include)
include_directories(/usr/include)
add_definitions("-Wall -g ")
aux_source_directory(. SRC_LIST)
add_executable(${PROJECT_NAME} ${SRC_LIST})
target_link_libraries(${PROJECT_NAME} -lcrypto -lssl)
六、运行结果
1)在client目录下创建build,并在build目录下执行cmake ../
cd ~/work/onvif/client
mkdir build
cd build
cmake ../
2)生成了Makefile文件
3)使用make进行编译,得到ONVIF-DEMO
4)执行ONVIF-DEMO,发现了两个设备
5)打开ONVIF Device Test Tool进行验证,此时确实发现两个设备
七、调试过程注意事项
1)出现如下语法错误
error C2143:语法错误 : 缺少“{”(在“:”的前面)
error C2059:语法错误 : “:”
error C2143:语法错误 : 缺少“{”(在“:”的前面)
需要将工程中的.c文件改成.cpp文件即可
2)无法解析的外部命令错误soap_check_faultsubcode
在stdsoap2.h中,声明的soap_check_faultsubcode(structsoap *soap)函数在soapC.cpp中未实现, 可在soapC.cpp中添加如下实现
SOAP_FMAC3 const char * SOAP_FMAC4soap_check_faultsubcode(struct soap *soap)
{
soap_fault(soap);
if(soap->version == 2)
{
if(soap->fault->SOAP_ENV__Code &&soap->fault->SOAP_ENV__Code->SOAP_ENV__Subcode &&soap->fault->SOAP_ENV__Code->SOAP_ENV__Subcode)
returnsoap->fault->SOAP_ENV__Code->SOAP_ENV__Subcode->SOAP_ENV__Value;
return NULL;
}
returnsoap->fault->faultcode;
}
3) 无法解析的外部命令错误soap_check_faultdetail
在stdsoap2.h中,声明的soap_check_faultdetail(struct soap *soap)函数在soapC.cpp中未实现,,可在soapC.cpp中添加如下实现
SOAP_FMAC3 const char * SOAP_FMAC4soap_check_faultdetail(struct soap *soap)
{
soap_fault(soap);
if(soap->version == 2 && soap->fault->SOAP_ENV__Detail)
returnsoap->fault->SOAP_ENV__Detail->__any;
if(soap->fault->detail)
return soap->fault->detail->__any;
returnNULL;
}
4) 出现无法解析的外部符号_soap_in_xsd__duration
无法解析的外部符号_soap_in_xsd__duration,该符号在函数_soap_getelement中被引用soapC.obj : error LNK2019: 无法解析的外部符号_soap_out_xsd__duration,该符号在函数_soap_putelement中被引用
soapC.obj: error LNK2019: 无法解析的外部符号_soap_default_xsd__duration,该符号在函数_soap_default__tse__FindMetadata中被引用
需要将custom文件夹下面的duration.h和duration.c导入工程中
5)如果是调用soap_call_XXXX_Probe()来实现设备发现时,不能发现所有onvif设备
该函数实现过程中只有一次接收过程,所以无法发现所有的设备的问题。如果使用该函数,还需要对函数的实现做以下更改:
函数的接收部分,将原来的XXXX:Response更改为YYYY:ProbeMatches
其中,
- XXXX是.nsmap文件中http://www.onvif.org/ver10/network/wsdl"所对应的命名空间前缀
- YYYY与后面YYYY:ProbeMatchesType中的前缀相同,都是Web Services Dynamic Discovery (WS-Discovery)所对应的命名空间前缀名
6)在VS中出现fatal error C1128: 节数超过对象文件格式限制:请使用/bigobj 进行编译的错误
这是由于源代码文件太大,需添加选项/bigobj,在项目属性-> C/C++ ->命令行的附加选项中添加/bigobj
八、总结
1)了解gsoap工具的使用方法、编程方法以及文件结构,参考Genivia - gSOAP 用户指南,可根据需要查找相关内容
2)学会利用抓包工具,例如Wireshark,用于分析客户端所发出的消息,以及服务端回复的消息