ONVIF协议开发之网络摄像头云台控制(C版)

在之前的文章中(《python-onvif实现客户端控制相机云台》),介绍过用python实现基于onvif协议的相机云台控制,考虑到嵌入式端的执行效率问题,还是需要实现C/C++版本的接口,因此尝试这方面的工作。经过将近一周的折腾,终于调通了onvif协议云台控制的代码,里边遇坑无数,从一个对onvif一无所知的小白,到最后顺利调通功能,还是有所收获的,将过程记录下来,给其他同学减少入坑的次数,目的也就达到了。话不多说,直接进入正题

1 预备知识

关于onvif的背景介绍,网上有很多资料,我这里就不具体展开,我之前也转载过一篇,《基于ONVIF协议的摄像头开发总结》,对onvif的原理有一个大概的介绍,而且许振坪博主的专栏也介绍的很详细,对onvif不了解的可以参考他的前几篇介绍文章。

2 开发流程

首先保证网络相机支持onvif协议,并且启用该协议,设置方法参考《海康相机之onvif测试工具使用》;

然后使用gsoap工具生成onvif代码框架;编写客户端程序,实现云台控制,客户端程序的调用流程会在后边详细介绍。

我要实现的是在嵌入式linux上开发客户端,来实现网络摄像头的云台控制。其它平台如x86、windows等平台流程类似,也同样可以采用类似的流程。

3 gsoap安装

用gsoap生成onvif代码框架的时候,主要用到wsdl2h和soapcpp2两个可执行文件,在gsoap/bin目录下,官方已经编译好win32和macosx平台的可执行文件,但linux下需要自己编译,因此我们使用源码编译的方法来生成这两个工具。

3.1 环境依赖

在编译之前,需要先安装OpenSSL,以便得到开启SSL/TLS的wsdl2h文件。建议先把系统中的SSL卸载掉重新安装,避免后边报错,卸载方法

sudo apt-get purge openssl

下载链接:https://www.openssl.org/source/,下载1.0.2版本并解压到home目录下

ONVIF协议开发之网络摄像头云台控制(C版)_第1张图片

进入目录安装

cd /openssl-1.0.2r
./config
sudo make
sudo make install

3.2 安装gsoap

下载地址:https://sourceforge.net/projects/gsoap2/

下载后解压到home目录下,然后按照以下指令安装

$ cd gsoap-2.8
$ sudo make distclean
$ sudo ./configure --with-openssl=/usr/local/ssl/lib --host=arm-linux
$ sudo make
$ sudo make install

说明:

--with-openssl的路径要与自己环境中的路径保持一致,如果是按照我上边源码安装SSL的方法,路径就是/usr/local/ssl/lib。

--host用来指定生成Gsoap工具的编译器,如果是在x86上面执行的,则默认的就可以,即不用配置。

--prefix用来指定生成工具的路径,采用默认即可。

查看安装路径

$ which wsdl2h
/usr/local/bin/wsdl2h

找到文件夹/usr/local/bin,找到wsdl2h和soapcpp2这两个可执行文件,拷贝到/home/gsoap-2.8/gsoap/bin路径下,方便我们使用。

4 gsoap生成代码框架

经过上边的步骤,环境已经准备完毕,接下来就可以生成代码框架了

4.1 wsdl文件准备

下载地址:https://www.onvif.org/profiles/specifications/

在Onvif官网Specification页面中下载提供的功能相应的wsdl文件,如analytics.wsdl;devicemgmt.wsdl等。直接将WSDL的链接另存为,保存下来就是wsdl文件了。因为不太确定哪些文件不需要,所以我这里全部都下载了,全部放在bin下新建的wsdl文件夹内,包括这些wsdl中需要调用的xsd文件, 

ONVIF协议开发之网络摄像头云台控制(C版)_第2张图片

在gsoap2.8/gsoap/bin目录下,新建一个文件夹wsdl,用来存放刚才下载的所有wsdl和xsd文件。

4.2 onvif.h文件生成

这一步我们用到的工具是wsdl2h,我们要实现PTZ控制,有几个主要的wsdl文件就够了,我用到的是device.wsdl、event.wsdl、media.wsdl、ptz.wsdl,cd进入bin目录,使用如下指令

./wsdl2h -c -t ../typemap.dat -o onvif.h ./wsdl/devicemgmt.wsdl ./wsdl/event.wsdl ./wsdl/media.wsdl ./wsdl/ptz.wsdl

各个选项的含义,可通过wsdl2h -help查看帮助。其中-c为产生纯c代码,默认为c++代码,-t为typemap.dat的标识。

有一个小技巧,如果网络条件不好的话,可以把wsdl文件中的schemaLocation的路径修改为本地文件路径,前提是下载好对应的文件,这样会速度快一些。

运行成功后,生成onvif.h文件,由于摄像头需要鉴权认证,需要在该文件头部添加如下代码

#import "wsse.h"

4.3 c文件生成

接下来使用onvif.h文件来生成c文件,用到的工具是soapcpp2,运行如下代码

./soapcpp2 -c -x onvif.h -I ../ -I ../import -I ../custom

其中,-c表示只生成c代码,-x表示不要产生XML示例文件,-I是包含的路径,运行成功后,会生成如下文件

soapC.c、soapH.h、soapClient.c、soapClientLib.c、soapStub.h、soapServer.c、soapServerLib.c、*.nsmap

新建一个文件夹onvif,把生成的这些文件全部拷贝到onvif文件夹中,其中*.nsmap文件内容都一样,我们只保留一个PTZBingding.nsmap即可。

5 PTZ控制

5.1 环境准备

在onvif文件夹中,继续添加文件,cd到上一级目录,直接使用cp命令来复制

$ cd gsoap-2.8/gsoap
$ cp stdsoap2.c stdsoap2.h dom.c plugin/wsaapi.c plugin/wsaapi.h plugin/wsseapi.c plugin/wsseapi.h plugin/mecevp.c plugin/mecevp.h plugin/smdevp.c plugin/smdevp.h plugin/threads.c plugin/threads.h custom/duration.c custom/duration.h  bin/onvif/

最终onvif文件夹中的文件内容如下

dom.c       onvif.h           soapC.c          soapServerLib.c  threads.h
duration.c  PTZBinding.nsmap  soapClient.c     soapStub.h       wsaapi.c
duration.h  README.txt        soapClientLib.c  stdsoap2.c       wsaapi.h
mecevp.c    smdevp.c          soapH.h          stdsoap2.h       wsseapi.c
mecevp.h    smdevp.h          soapServer.c     threads.c        wsseapi.h

5.2 控制流程

1、通过设备服务地址(形如http://xx/onvif/device_service),调用GetCapabilities函数接口,获取到Media的URL;

2、通过Media的URL,调用GetProfiles函数接口,获取到ProfileToken;

3、对_tptz__AbsoluteMove结构体进行填充;

4、调用soap_call___tptz__AbsoluteMove函数接口实现摄像头转动功能;

5.3 代码实现

在bin文件夹下新建一个PTZ文件夹,新建myptz.c文件,内容如下

#include 
#include 
#include 
#include "soapH.h"
#include "wsseapi.h"
#include "wsaapi.h"
#include "PTZBinding.nsmap"

//宏定义设备鉴权的用户名和密码
//注意对于海康相机而言,鉴权的用户名和密码需要单独设置,不一定等同于登录账户密码
//设置方法参考https://zongxp.blog.csdn.net/article/details/89632354
#define USERNAME    "admin"
#define PASSWORD    "123456"

int main(int argc, char** argv)
{
    struct soap soap;
	soap_init(&soap);
			
    char * ip;
    char Mediaddr[256]="";
    char profile[256]="";
    float pan = 1;
    float panSpeed = 1;
    float tilt = 1;
    float tiltSpeed = 0.5;
    float zoom = 0;
    float zoomSpeed = 0.5;
    struct _tds__GetCapabilities            	req;
    struct _tds__GetCapabilitiesResponse    	rep;
    struct _trt__GetProfiles 			getProfiles;
    struct _trt__GetProfilesResponse		response;	
    struct _tptz__AbsoluteMove           absoluteMove;
    struct _tptz__AbsoluteMoveResponse   absoluteMoveResponse;
	       	
    req.Category = (enum tt__CapabilityCategory *)soap_malloc(&soap, sizeof(int));
    req.__sizeCategory = 1;
    *(req.Category) = (enum tt__CapabilityCategory)0;
       
    //第一步:获取capability
    char endpoint[255];
    memset(endpoint, '\0', 255);
    if (argc > 1)
    {
        ip = argv[1];
    }
    else
    {
        ip = "192.168.170.248"; 
    }
    sprintf(endpoint, "http://%s/onvif/device_service", ip);    
    soap_call___tds__GetCapabilities(&soap, endpoint, NULL, &req, &rep);
    if (soap.error)  
    {  
        printf("[%s][%d]--->>> soap result: %d, %s, %s\n", __func__, __LINE__, 
	                                        soap.error, *soap_faultcode(&soap), 
	                                        *soap_faultstring(&soap));  	 
    } 
    else
	{
        printf("get capability success\n");
        //printf("Dev_XAddr====%s\n",rep.Capabilities->Device->XAddr);
        printf("Med_XAddr====%s\n",rep.Capabilities->Media->XAddr);
        //printf("PTZ_XAddr====%s\n",rep.Capabilities->PTZ->XAddr);
        strcpy(Mediaddr,rep.Capabilities->Media->XAddr);
    }	
    printf("\n");
	
    //第二步:获取profile,需要鉴权	
    //自动鉴权
    soap_wsse_add_UsernameTokenDigest(&soap, NULL, USERNAME, PASSWORD);
	
    //获取profile
    if(soap_call___trt__GetProfiles(&soap,Mediaddr,NULL,&getProfiles,&response)==SOAP_OK)
    {
        strcpy(profile, response.Profiles[0].token);
        printf("get profile succeed \n");		
	    printf("profile====%s\n",profile);	
    }
    else
    {
        printf("get profile failed \n");
	    printf("[%s][%d]--->>> soap result: %d, %s, %s\n", __func__, __LINE__, 
	                                        soap.error, *soap_faultcode(&soap), 
	                                        *soap_faultstring(&soap));  
    }
    printf("\n");	
		
    //第三步:PTZ结构体填充
    char PTZendpoint[255];
    memset(PTZendpoint, '\0', 255);
    sprintf(PTZendpoint, "http://%s/onvif/PTZ", ip);
    printf("PTZendpoint is %s \n", PTZendpoint);        
    
    absoluteMove.ProfileToken = profile;
    //setting pan and tilt
    absoluteMove.Position = soap_new_tt__PTZVector(&soap, -1);
    absoluteMove.Position->PanTilt = soap_new_tt__Vector2D(&soap, -1);
    absoluteMove.Speed = soap_new_tt__PTZSpeed(&soap, -1);
    absoluteMove.Speed->PanTilt = soap_new_tt__Vector2D(&soap, -1);
    //pan
    absoluteMove.Position->PanTilt->x = pan;
    absoluteMove.Speed->PanTilt->x = panSpeed;
    //tilt
    absoluteMove.Position->PanTilt->y = tilt;
    absoluteMove.Speed->PanTilt->y = tiltSpeed;
    //setting zoom
    absoluteMove.Position->Zoom = soap_new_tt__Vector1D(&soap, -1);
    absoluteMove.Speed->Zoom = soap_new_tt__Vector1D(&soap, -1);
    absoluteMove.Position->Zoom->x = zoom;
    absoluteMove.Speed->Zoom->x = zoomSpeed;
    
    //第四步:执行绝对位置控制指令,需要再次鉴权
    soap_wsse_add_UsernameTokenDigest(&soap, NULL, USERNAME, PASSWORD);
    soap_call___tptz__AbsoluteMove(&soap, PTZendpoint, NULL, &absoluteMove, 
	                                        &absoluteMoveResponse);			 
    //第五步:清除结构体
    soap_destroy(&soap); // clean up class instances
    soap_end(&soap); // clean up everything and close socket, // userid and passwd were deallocated
    soap_done(&soap); // close master socket and detach context
    printf("\n");	
		
    return 0;
}

新建一个makefile文件,内容如下

include Makefile.inc

PROGRAM = PTZ

SOURCES += myptz.c

OBJECTS := $(patsubst %.c,$(TEMPDIR)%.o,$(filter %.c, $(SOURCES)))

all: $(OBJECTS_ONVIF) $(OBJECTS_COMM) $(OBJECTS)
	$(CC) -o $(PROGRAM) $(OBJECTS_ONVIF) $(OBJECTS_COMM) $(OBJECTS) $(LDLIBS)

clean:
	rm -f $(OBJECTS_ONVIF)
	rm -f $(OBJECTS_COMM)
	rm -f $(OBJECTS)
	rm -f $(PROGRAM)

新建Makefile.inc文件

SHELL = /bin/bash

CC           := gcc 
CPP          := g++
LD           := ld
AR           := ar
STRIP        := strip


CFLAGS += -c -g -Wall -DWITH_DOM -DWITH_OPENSSL -DDEBUG
CFLAGS += $(INCLUDE)

# openssl目录名
OPENSSL_DIR = /usr/local/ssl
# 源文件
SOURCES_ONVIF += \
           ../onvif/soapC.c                          \
           ../onvif/soapClient.c                     \
           ../onvif/stdsoap2.c                       \
           ../onvif/wsaapi.c                         \
           ../onvif/dom.c                            \
           ../onvif/mecevp.c                         \
           ../onvif/smdevp.c                         \
           ../onvif/threads.c                        \
           ../onvif/wsseapi.c                        \

# 目标文件
OBJECTS_ONVIF := $(patsubst %.c,$(TEMPDIR)%.o,$(filter %.c, $(SOURCES_ONVIF)))

# 头文件路径
INCLUDE += -I../onvif/                               \
           -I$(OPENSSL_DIR)/include                  \

# 静态库链接OpenSSL
LDLIBS += $(OPENSSL_DIR)/lib/libssl.a                \
          $(OPENSSL_DIR)/lib/libcrypto.a             \
          -ldl                                       \

# 链接库(其他)
LDLIBS += -lpthread

%.o: %.cpp
	@echo "  CPP     " $@;
	@$(CPP) $(CFLAGS) -c -o $@ $<

%.o: %.c
	@echo "  CC      " $@;
	@$(CC) $(CFLAGS) -c -o $@ $<

.PHONY: all clean

然后运行make,即可在PTZ文件夹中生成PTZ可执行文件,运行该文件,可在目录下生成log日志文件,如果报错的话,可查看日志文件。注意如果是在嵌入式端部署的话,一定不要加-DDEBUG选项,否则日志文件会越来越大,影响板子性能

6 其它

6.1 关于鉴权

有很多onvif接口在调用之前需要鉴权(即调用soap_wsse_add_UsernameTokenDigest(&soap, NULL, USERNAME, PASSWORD)函数),并且鉴权完一次之后还需要重新鉴权,具体可参考《ONVIF协议网络摄像机(IPC)客户端程序开发(9):鉴权(认证)》,总结一下,只有以下接口是可以在不认证的情况下调用,一定要注意。

  • GetWsdlUrl
  • GetServices
  • GetServiceCapabilities
  • GetCapabilities
  • GetHostname
  • GetSystemDateAndTime
  • GetEndpointReference
  • GetRelayOutputOptions

6.2 segmentation fault错误

这个错误很常见,主要是由于访问了没有分配地址的内存导致的,在填充功能函数时,很容易漏掉为必须的结构体分配内存,导致gSoap产生的代码会在不知情的状况下访问该结构体,然后报segmentation fault错误,需要注意这点

6.3 其它控制

本文实现的是PTZ绝对控制,当然onvif还支持其它模式,具体实现代码可参考这个链接进行实现

6.4 结构体描述

关于结构体的描述,可参考这篇文章。

你可能感兴趣的:(视频开发)