介绍
在安防摄像头中,不仅仅涉及到固定摄像头的枪击,同样还包含可以360°转动的球机。因此对球机的云台方向控制是Onvif协议开发过程中必不可少的过程
球机的云台控制主要包含:八个方向(上、下、左、右、左上、左下、右上、右下),放大、缩小等,这在个过程中还包含对转动速度的控制或者放大缩小的速度控制。对应的方向及正负值如下图:
编码流程
1、通过设备服务地址(形如http://xx/onvif/device_service),调用GetCapabilities函数接口,获取到Ptz的URL;
2、通过Ptz的URL,调用GetProfiles函数接口,获取到ProfileToken;
3、对_tptz__AbsoluteMove结构体进行填充;
4、调用soap_call___tptz__AbsoluteMove函数接口实现摄像头转动功能;
实践
具体请参见
除了onvif_head.sh
修改为:
#!/bin/bash
mkdir onvif_head
cd onvif_head
../bin/wsdl2h -o onvif.h -s -d -x -t ../gsoap/WS/typemap.dat \
http://www.onvif.org/onvif/ver10/network/wsdl/remotediscovery.wsdl \
https://www.onvif.org/onvif/ver10/device/wsdl/devicemgmt.wsdl \
http://www.onvif.org/onvif/ver10/media/wsdl/media.wsdl \
http://www.onvif.org/onvif/ver20/ptz/wsdl/ptz.wsdl
main.cpp
修改为:
#include <assert.h>
#include "soapH.h"
#include "wsdd.nsmap"
#include "soapStub.h"
#include "wsseapi.h"
#include "wsaapi.h"
#include <map>#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超时时间(单秒秒)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的namespacessoap->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
#endifsoap_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 datasoap_done(soap); // Reset, close communications, and remove callbackssoap_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 { // 成功接收到设备的应答消息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 ) {std::string url = probeMatch->XAddrs;if(url == "http://192.168.0.116/onvif/device_service"){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, nullptr, username, password);SOAP_CHECK_ERROR(result, soap, "add_UsernameTokenDigest");EXIT: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:password@100.100.100.140:554/av0_0
************************************************************************/
static int make_uri_withauth(const std::string& src_uri, const std::string&username, const std::string&password, std::string *dest_uri)
{int result = 0;SOAP_ASSERT(!src_uri.empty());if (username.empty() &&password.empty()) { // 生成新的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;
}#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;_tds__GetDeviceInformation devinfo_req;_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_GetCapabilities
**功能:获取设备能力信息
**参数:[in] DeviceXAddr - 设备服务地址[in]
**返回:0表明成功,非0表明失败
**备注:1). 其中最主要的参数之一是媒体服务地址
************************************************************************/
int ONVIF_GetCapabilities(const std::string& deviceXAddr, std::string * ptzXAddr)
{int result = 0;struct soap *soap = nullptr;_tds__GetCapabilities devinfo_req;_tds__GetCapabilitiesResponse devinfo_resp;SOAP_ASSERT(!deviceXAddr.empty());SOAP_ASSERT(nullptr != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));result = soap_call___tds__GetCapabilities(soap, deviceXAddr.c_str(), nullptr, &devinfo_req, devinfo_resp);SOAP_CHECK_ERROR(result, soap, "GetCapabilities");if(devinfo_resp.Capabilities->PTZ != nullptr){*ptzXAddr = devinfo_resp.Capabilities->PTZ->XAddr;}EXIT:if (nullptr != soap) {ONVIF_soap_delete(soap);}return result;
}int ONVIF_GetProfiles(const std::string& ptzXAddr, std::string * profilesToken)
{int result = 0;struct soap *soap = nullptr;_trt__GetProfiles devinfo_req;_trt__GetProfilesResponse devinfo_resp;SOAP_ASSERT(!ptzXAddr.empty());SOAP_ASSERT(nullptr != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);result = soap_call___trt__GetProfiles(soap, ptzXAddr.c_str(), nullptr, &devinfo_req, devinfo_resp);SOAP_CHECK_ERROR(result, soap, "ONVIF_GetProfiles");SOAP_ASSERT(devinfo_resp.__sizeProfiles > 0);*profilesToken = devinfo_resp.Profiles[0]->token;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 std::string& MediaXAddr, const std::string& ProfileToken, std::string * snapUri)
{int result = 0;struct soap *soap = nullptr;_trt__GetSnapshotUri req;_trt__GetSnapshotUriResponse rep;SOAP_ASSERT(!MediaXAddr.empty() && !ProfileToken.empty());SOAP_ASSERT(nullptr != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);req.ProfileToken = const_cast<char *>(ProfileToken.c_str());result = soap_call___trt__GetSnapshotUri(soap, MediaXAddr.c_str(), nullptr, &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;
}
// 获取当前ptz的位置以及状态
int ONVIF_PTZ_GetStatus(const std::string& ptzXAddr, const std::string& ProfileToken){int result = 0;struct soap *soap = nullptr;_tptz__GetStatus getStatus;_tptz__GetStatusResponse getStatusResponse;SOAP_ASSERT(!ptzXAddr.empty());SOAP_ASSERT(nullptr != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);getStatus.ProfileToken = const_cast<char *>(ProfileToken.c_str());result = soap_call___tptz__GetStatus(soap, ptzXAddr.c_str(), nullptr, &getStatus, getStatusResponse);SOAP_CHECK_ERROR(result, soap, "ONVIF_PTZ_GetStatus");if(*getStatusResponse.PTZStatus->MoveStatus->PanTilt == tt__MoveStatus__IDLE){std::cout << " 空闲 ... " << std::endl;}else if(*getStatusResponse.PTZStatus->MoveStatus->PanTilt == tt__MoveStatus__MOVING){std::cout << " 移动中 ... " << std::endl;}else if(*getStatusResponse.PTZStatus->MoveStatus->PanTilt == tt__MoveStatus__UNKNOWN){std::cout << " 未知 ... " << std::endl;}std::cout << "当前p: " <<getStatusResponse.PTZStatus->Position->PanTilt->x << "\n";std::cout << "当前t: " << getStatusResponse.PTZStatus->Position->PanTilt->y << "\n";std::cout << "当前z: " << getStatusResponse.PTZStatus->Position->Zoom->x << "\n";EXIT:if (nullptr != soap) {ONVIF_soap_delete(soap);}return 0;
}// 以指定速度移动到指定位置的ptz
// p : -1 ~ 1 []
// t : -1 ~ 1
// z : 0 ~ 1
int ONVIF_PTZAbsoluteMove(const std::string& ptzXAddr, const std::string& ProfileToken){int result = 0;struct soap *soap = nullptr;_tptz__AbsoluteMove absoluteMove;_tptz__AbsoluteMoveResponse absoluteMoveResponse;SOAP_ASSERT(!ptzXAddr.empty() && !ProfileToken.empty());SOAP_ASSERT(nullptr != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);absoluteMove.ProfileToken = const_cast<char *>(ProfileToken.c_str());absoluteMove.Position = soap_new_tt__PTZVector(soap);absoluteMove.Position->PanTilt = soap_new_tt__Vector2D(soap);absoluteMove.Position->Zoom = soap_new_tt__Vector1D(soap);absoluteMove.Speed = soap_new_tt__PTZSpeed(soap);absoluteMove.Speed->PanTilt = soap_new_tt__Vector2D(soap);absoluteMove.Speed->Zoom = soap_new_tt__Vector1D(soap);absoluteMove.Position->PanTilt->x = 0.440833; // pabsoluteMove.Position->PanTilt->y = 0.583455; // tabsoluteMove.Position->Zoom->x = 0.0333333; // z// x 和y的绝对值越接近1,表示云台的速度越快absoluteMove.Speed->PanTilt->x = 0.5;absoluteMove.Speed->PanTilt->y = 0.5;absoluteMove.Speed->Zoom->x = 0.5;result = soap_call___tptz__AbsoluteMove(soap,ptzXAddr.c_str(), nullptr,&absoluteMove,absoluteMoveResponse);SOAP_CHECK_ERROR(result, soap, "ONVIF_PTZAbsoluteMove");EXIT:if (nullptr != soap) {ONVIF_soap_delete(soap);}return 0;
}int ONVIF_PTZStopMove(const std::string& ptzXAddr, const std::string& ProfileToken){int result = 0;struct soap *soap = nullptr;_tptz__Stop tptzStop;_tptz__StopResponse tptzStopResponse;SOAP_ASSERT(!ptzXAddr.empty() && !ProfileToken.empty());SOAP_ASSERT(nullptr != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);tptzStop.ProfileToken = const_cast<char *>(ProfileToken.c_str());result = soap_call___tptz__Stop(soap, ptzXAddr.c_str(), nullptr, &tptzStop, tptzStopResponse);SOAP_CHECK_ERROR(result, soap, "ONVIF_PTZStopMove");EXIT:if (nullptr != soap) {ONVIF_soap_delete(soap);}return result;
}enum PTZCMD
{PTZ_CMD_LEFT,PTZ_CMD_RIGHT,PTZ_CMD_UP,PTZ_CMD_DOWN,PTZ_CMD_LEFTUP,PTZ_CMD_LEFTDOWN,PTZ_CMD_RIGHTUP,PTZ_CMD_RIGHTDOWN,PTZ_CMD_ZOOM_IN,PTZ_CMD_ZOOM_OUT,
};
// speed --> (0, 1]
int ONVIF_PTZContinuousMove(const std::string& ptzXAddr, const std::string& ProfileToken, enum PTZCMD cmd, float speed){int result = 0;struct soap *soap = nullptr;_tptz__ContinuousMove continuousMove;_tptz__ContinuousMoveResponse continuousMoveResponse;SOAP_ASSERT(!ptzXAddr.empty() && !ProfileToken.empty());SOAP_ASSERT(nullptr != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);continuousMove.ProfileToken = const_cast<char *>(ProfileToken.c_str());continuousMove.Velocity = soap_new_tt__PTZSpeed(soap);continuousMove.Velocity->PanTilt = soap_new_tt__Vector2D(soap);continuousMove.Velocity->Zoom = soap_new_tt__Vector1D(soap);switch (cmd){case PTZ_CMD_LEFT:continuousMove.Velocity->PanTilt->x = -speed;continuousMove.Velocity->PanTilt->y = 0;break;case PTZ_CMD_RIGHT:continuousMove.Velocity->PanTilt->x = speed;continuousMove.Velocity->PanTilt->y = 0;break;case PTZ_CMD_UP:continuousMove.Velocity->PanTilt->x = 0;continuousMove.Velocity->PanTilt->y = speed;break;case PTZ_CMD_DOWN:continuousMove.Velocity->PanTilt->x = 0;continuousMove.Velocity->PanTilt->y = -speed;break;case PTZ_CMD_LEFTUP:continuousMove.Velocity->PanTilt->x = -speed;continuousMove.Velocity->PanTilt->y = speed;break;case PTZ_CMD_LEFTDOWN:continuousMove.Velocity->PanTilt->x = -speed;continuousMove.Velocity->PanTilt->y = -speed;break;case PTZ_CMD_RIGHTUP:continuousMove.Velocity->PanTilt->x = speed;continuousMove.Velocity->PanTilt->y = speed;break;case PTZ_CMD_RIGHTDOWN:continuousMove.Velocity->PanTilt->x = speed;continuousMove.Velocity->PanTilt->y = -speed;break;case PTZ_CMD_ZOOM_IN:continuousMove.Velocity->PanTilt->x = 0;continuousMove.Velocity->PanTilt->y = 0;continuousMove.Velocity->Zoom->x = speed;break;case PTZ_CMD_ZOOM_OUT:continuousMove.Velocity->PanTilt->x = 0;continuousMove.Velocity->PanTilt->y = 0;continuousMove.Velocity->Zoom->x = -speed;break;default:break;}// 也可以使用soap_call___tptz__RelativeMove实现result = soap_call___tptz__ContinuousMove(soap,ptzXAddr.c_str(), nullptr,&continuousMove,continuousMoveResponse);SOAP_CHECK_ERROR(result, soap, "ONVIF_PTZAbsoluteMove");
/* sleep(1); //如果当前soap被删除(或者发送stop指令),就会停止移动ONVIF_PTZStopMove(ptzXAddr, ProfileToken);*/EXIT:if (nullptr != soap) {ONVIF_soap_delete(soap);}return result;
}void cb_discovery(char *deviceXAddr)
{std::string ptzXAddr, profilesToken, snapUri, snapAuthUri;ONVIF_GetCapabilities(deviceXAddr, &ptzXAddr);ONVIF_GetProfiles(ptzXAddr, &profilesToken);// ONVIF_PTZ_GetStatus(ptzXAddr, profilesToken);// ONVIF_PTZAbsoluteMove(ptzXAddr, profilesToken);ONVIF_PTZContinuousMove(ptzXAddr, profilesToken, PTZ_CMD_LEFTUP, 0.3);
}int main(int argc, char **argv)
{ONVIF_DetectDevice(cb_discovery);return 0;
}
参考
Onvif协议客户端开发(8)–球机云台的控制
ONVIF PTZ云台控制–RelativeMove
初学小结使用Onvif协议进行PTZ控制
Onvif PTZ简介
使用Onvif协议进行设备PTZ云台控制
ONVIF PTZ控制
其他文章
- Onvif协议:理解什么是Web Services
- Onvif协议:使用gSOAP创建SOAP调用实例
- Onvif协议:门外汉理解ONVIF协议
- Onvif协议:到底什么是ONVIF协议
- Onvif协议:实现Probe命令来进行设备发现(discover)
- Onvif协议:IPC客户端开发之获取设备基本信息
- Onvif协议:IPC客户端开发之鉴权
- Onvif协议:IPC客户端开发之获取设备能力
- Onvif协议:IPC客户端开发之PTZ控制
- Onvif协议:IPC客户端开发之获取实时预览的Url地址
- Onvif协议:IPC客户端开发之图像抓拍
- ONVIF Device Test Tool测试工具使用方法