Linux c++ onvif客户端开发(1): 根据wsdl生成cpp源文件

Onvif是没有开源库的,在C++上开发onvif相关功能必须使用gSOAP工具根据wsdl文件来生成相关源代码文件。这一过程有些繁琐,但是也带了了很多灵活性——功能是自定义的。

编译gSOAP

下载

gSoap分商业版和开源版本,开源版可以从gSOAP Toolkit download | SourceForge.net下载

要用gsoap生成onvif源码,必须用到wsdl2h和soapcpp2两个工具(执行文件)。

wsdl2h默认不支持HTTPS,编译的时候需要开启https支持——默认编译支持。

也就是说我们最好从源代码去编译安装,然后就可以直接使用https的在线wsdl文档了。

安装依赖

gSOAP有三个依赖需要安装

sudo apt install bison flex openssl

也可以从上面地址去下载自己编译安装依赖,甚至不装。

编译gSOAP

找个目录解压刚刚下载的gSOAP源码包,进入目录然后编译安装。

> ./configure
> make -j4
> sudo make install

根据wsdl文档编译onvif源代码

这里的所有步骤我已经加入了开源项目https://github.com/NoevilMe/onvif_demo.git

也有比较详细的文档说明。

先确定一个目录,这里不是gsoap的源代码目录,是用来写测试代码的地方,名字任取吧。

复制会用到的源代码

在确定的工作目录下,创建一些子目录用来进行后续的工作

.
├── gsoap
│   ├── custom
│   ├── import
│   └── plugin
├── onvif
├── onvif_head
├── soap
└── tests

或者可以将下面的代码保存成 copy_from_source.sh ,然后运行。注意其中SRC_DIR必须修改成你自己的解压出来的包目录。

# 将要用到的一些源码文件复制到gsoap目录

# 自己修改源码目录
SRC_DIR=~/opensource/gsoap-2.8

if [ ! -d $SRC_DIR ]; then
    echo ${SRC_DIR} does not exist!
    exit 1
fi

FROM_DIR=$SRC_DIR/gsoap
if [ ! -d $FROM_DIR ]; then
    echo ${FROM_DIR} does not exist!
    exit 1
fi

if [ ! -d gsoap ]; then
    mkdir gsoap
fi

TARGET_DIR=gsoap

# 要复制的目录和文件
LISTS="custom import plugin dom.cpp stdsoap2.h stdsoap2.cpp"
for t in ${LISTS}; do
    ft=${FROM_DIR}/${t}
    if [ -d ${ft} ]; then
        echo ${ft} is an directory
        cp -rf ${ft} ${TARGET_DIR}
    elif [ -f ${ft} ]; then
        echo ${ft} is a file
        cp -rf ${ft} ${TARGET_DIR}
    else
        echo ${ft} does not exist!
    fi
done

# typemap.dat 不要覆盖,这个会被修改了
if [ ! -f gsoap/typemap.dat ]; then
    cp ${FROM_DIR}/typemap.dat gsoap/typemap.dat
else
    echo "typemap.dat exists! do not copy it!"
fi

这一步会将一些生成onvif源码过程中用到的gsoap源代码复制到gsoap目录下

- gsoap/import

- gsoap/custom

- gsoap/plugin

- gsoap/stdsoap2.cpp

- gsoap/stdsoap2.h

- gsoap/typemap.dat

- ...

修改typemap.dat

由于后续编译过程中需要用到 duration.c 文件,会遇到类型LONG64报错的问题,需要gsoap/typemap.dat 文件中取消以下行的注释:

xsd__duration = #import “custom/duration.h” | xsd__duration

生成头文件onvif.h

执行命令

./step1_gen_head.sh

此步骤会生成onvif_head/onvif.h文件

该脚本会在线下载wsdl文件(需要自己配置),并且修改onvif.h文件,加入鉴权的相关项。

命令解析

step1_gen_head.sh主要使用了wsdl2h命令来生成onvif.h文件。wsdl2h参数解析:

-c : 生成c风格代码(注:后缀名还是.cpp ,但实际上是.c)

-c++:生成c++风格代码(注 : 默认是生成c++代码)

-x : 表示不生成xml 文件(注:生成的xml文件,有助于了解发送是SOAP是怎样的结构,建议不使用-x)

-l : 表示指定导入路径

-C : 表示生成客户端代码

-S : 表示生成服务端代码

-s : 不使用STL代码

-o: 生成.h文件叫什么名字

-t : 后面紧跟“typemap.dat”这个批处理文件

The wsdl2h tool performs the mapping of WSDL and XML schemas to C and/or C++ automatically. The output of wsdl2h is a "data binding interface file" which is simply an annotated C/C++ header file with the serializable C/C++ data types that represent XML schema components. This file also includes comments and documentation of the serializable data types.

生成的onvif.h是一个数据绑定接口定义文件,后续步骤会用到。

关于鉴权

鉴权也就是设备使用用户名密码登录。

如果onvif.h不加入#import "wsse.h",使用soap_wsse_add_UsernameTokenDigest函数会导致编译出错,也就无法登录设备进行操作了。

但是默认生成的onvif.h中是没有#import "wsse.h"的。step1_gen_head.sh脚本已经处理了这个问题。

#加入鉴权,发送请求需要用户名和密码
sed -i '122 a #import "wsse.h"' ${DST}

wsdl的相关功能描述

  • - https://www.onvif.org/ver10/device/wsdl/devicemgmt.wsdl 用于获取设备参数
  • - https://www.onvif.org/onvif/ver10/network/wsdl/remotediscovery.wsdl 用于发现设备
  • - https://www.onvif.org/onvif/ver20/ptz/wsdl/ptz.wsdl 云台控制
  • - https://www.onvif.org/onvif/ver10/media/wsdl/media.wsdl 获取264的视频流地址
  • - https://www.onvif.org/onvif/ver20/media/wsdl/media.wsdl 获取h265视频流地址
  • - http://www.onvif.org/onvif/ver20/imaging/wsdl/imaging.wsdl 光圈,对比度,饱和度

更多的我暂时没用到,也就不举例了。

SOAP_ENV__Fault重复定义

如果没有修改相关文件,生成代码的时候会出现如下错误。

wsa5.h(280): *WARNING*: Duplicate declaration of 'SOAP_ENV__Fault' (already declared at line 268)

wsa5.h(290): **ERROR**: service operation name clash: struct/class 'SOAP_ENV__Fault' already declared at wsa.h:278

之所有会出现这个错误,是因为onvif.h头文件中同时:

 #import "wsdd10.h" // wsdd10.h中又#import "wsa.h"

 #import "wsa5.h"   // wsa.h和wsa5.h两个文件重复定义了int SOAP_ENV__Fault

 解决方法:

修改import\wsa5.h文件,将int SOAP_ENV__Fault修改为不冲突的任何名字,例如int SOAP_ENV__Fault_xxx,再次使用soapcpp2工具编译就成功了。

脚本也已经自动处理了,具体实现是

sed -i 's/int SOAP_ENV__Fault$/int SOAP_ENV__Fault_xxx/g' gsoap/import/wsa5.h

生成onvif相关源代码

执行生成命令

./step2_gen_code.sh

脚本已经删除了一些无用文件、复制并重命名了相关文件。其中onvif.h文件其实已经没用了,可以删掉,不需要参与后续IPC客户端程序的编译。这里有好多个命名空间的.nsmap文件,文件内容都一模一样,拿wsdd.nsmap一个来用即可。

soap目录

这里把从gsoap中将来用到的一些文件复制过来了,修改成了cpp后缀

onvif目录

一些生成的源代码文件,就是我们要的东西。

  • 各种nsmap文件:命名空间,除了名字不一样,内容是一样的,里面的内容竟然是每一个xml文件里的Envelope字段内容。我们只需要留下一个就可以了,并将之改名为wsdd.nsmap
  • soapC.cpp:指定数据结构的序列化和反序列化
  • soapClient.cpp:客户端代码
  • soapH.h:主头文件,所有客户机和服务器源代码都要包括它
  • soapStub.h:从输入头文件(onvif.h)生成的经过修改且带命名空间前缀的头文件

示例

还是参考工程NoevilMe/onvif_demo: Linux c++ onvif client demo (github.com)

CMakeLists

cmake_minimum_required(VERSION 3.0)
project(OnvifSoap)

set(CMAKE_CXX_STANDARD 11)

set(CMAKE_CXX_FLAGS "-g -O0")

set(LIB_SOAP_SRC
    soap/struct_timeval.cpp
    soap/duration.cpp
    soap/wsaapi.cpp
    soap/dom.cpp
    soap/wsseapi.cpp
    soap/smdevp.cpp
    soap/mecevp.cpp
    soap/threads.cpp
    soap/stdsoap2.cpp)

add_library(onvif_soap STATIC ${LIB_SOAP_SRC})
target_link_libraries(onvif_soap PUBLIC ssl crypto)
target_compile_definitions(onvif_soap PUBLIC WITH_OPENSSL WITH_DOM)
target_include_directories(onvif_soap PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/onvif)

add_library(onvif STATIC onvif/soapC.cpp onvif/soapClient.cpp)
target_include_directories(onvif PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(onvif PUBLIC onvif_soap)

add_executable(dev_scan tests/scan_device.cpp)
target_link_libraries(dev_scan PRIVATE onvif)

soap目录编译成了libonvif_soap,依赖openssl。 还需要定义两个宏WITH_OPENSSL和 WITH_DOM

onvif目录编译成了libonvif。 依赖libonvif_soap.

demo

位于tests/scan_device.cpp

#include "onvif/soapH.h"
#include "soap/wsaapi.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

#include "onvif/wsdd.nsmap"

#define SOAP_ASSERT assert
#define SOAP_DBGLOG printf
#define SOAP_DBGERR printf

#define SOAP_SOCK_TIMEOUT (10) // socket超时时间(单秒秒)

#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)

void soap_perror(struct soap *soap, const char *str) {

    //   if (soap->error)
    // soap_print_fault(soap, stderr);
    if (NULL == 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));
    }
    return;
}

#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_COMPAT_TYPES "dn:NetworkVideoTransmitter" // 寻找的设备类型

class OnvifSoap {
public:
    OnvifSoap(int timeout) {
        // There is no need to call soap_init to initialize the context
        // allocated with soap_new, since soap_new initializes the allocated
        // context.
        soap_ = soap_new();

        soap_set_namespaces(soap_, namespaces); // 设置soap的namespaces

        // 不正常数据设置成20s
        if (timeout <= 0)
            timeout = 20;

        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会乱码
    }
    ~OnvifSoap() {
        soap_destroy(
            soap_);      // deletes data, array, and other managed C++ objects
        soap_end(soap_); // delete managed memory。soap_malloc
        // soap_done(soap_); // Reset, close communications, and remove
        // callbacks
        soap_free(soap_); /* we're done with the context */
    }

    struct soap *soap() { return soap_; }

    void *Malloc(size_t n) {
        if (!n) {
            return nullptr;
        }
        // Allocate a block of heap memory managed by the specified soap context
        // All such blocks allocated are deleted with a single call to soap_end.
        auto p = soap_malloc(soap_, n);
        assert(p);
        return p;
    }

    const char *WsaRandUuid() { return soap_wsa_rand_uuid(soap_); }

    void InitHeader() {
        // T * soap_new_T(struct soap*) allocates and initializes data of type T
        // in context-managed heap memory, managed data is deleted with
        // soap_destroy (deletes C++ objects) and soap_end (deletes all other
        // data), and you can also use soap_malloc to allocate uninitialized
        // context-managed memory.
        struct SOAP_ENV__Header *header = soap_new_SOAP_ENV__Header(soap_);
        soap_default_SOAP_ENV__Header(soap_, header);

        header->wsa__MessageID = (char *)this->WsaRandUuid();
        header->wsa__To = (char *)this->Malloc(strlen(SOAP_TO) + 1);
        header->wsa__Action = (char *)this->Malloc(strlen(SOAP_ACTION) + 1);
        strcpy(header->wsa__To, SOAP_TO);
        strcpy(header->wsa__Action, SOAP_ACTION);
        soap_->header = header;
    }

    void InitProbeType(struct wsdd__ProbeType *probe) {
        // 用于描述查找哪类的Web服务
        struct wsdd__ScopesType *scope = soap_new_wsdd__ScopesType(soap_);

        soap_default_wsdd__ScopesType(soap_, scope); // 设置寻找设备的范围
        scope->__item = "";

        soap_default_wsdd__ProbeType(soap_, probe);
        probe->Scopes = scope;
        probe->Types = (char *)SOAP_COMPAT_TYPES; // 设置寻找设备的类型
    }

    int Error() { return soap_->error; }

private:
    struct soap *soap_;
};

void ONVIF_DetectDevice(void (*cb)(char *DeviceXAddr)) {
    int i;
    int result = 0;
    unsigned int count = 0;          // 搜索到的设备个数
    struct wsdd__ProbeType req;      // 用于发送Probe消息
    struct __wsdd__ProbeMatches rep; // 用于接收Probe应答
    struct wsdd__ProbeMatchType *probeMatch;

    OnvifSoap onvif_soap(SOAP_SOCK_TIMEOUT);
    onvif_soap.InitHeader();        // 设置消息头描述
    onvif_soap.InitProbeType(&req); // 设置寻找的设备的范围和类型

    result = soap_send___wsdd__Probe(onvif_soap.soap(), SOAP_MCAST_ADDR, NULL,
                                     &req); // 向组播地址广播Probe消息
    while (SOAP_OK == result) // 开始循环接收设备发送过来的消息
    {
        soap_default___wsdd__ProbeMatches(onvif_soap.soap(), &rep);
        result = soap_recv___wsdd__ProbeMatches(onvif_soap.soap(), &rep);
        if (SOAP_OK == result) {
            if (onvif_soap.Error()) {
                soap_perror(onvif_soap.soap(), "ProbeMatches");
            } else { // 成功接收到设备的应答消息
                if (NULL != rep.wsdd__ProbeMatches) {
                    count += rep.wsdd__ProbeMatches->__sizeProbeMatch;
                    for (i = 0; i < rep.wsdd__ProbeMatches->__sizeProbeMatch;
                         i++) {
                        probeMatch = rep.wsdd__ProbeMatches->ProbeMatch + i;
                        std::cout << probeMatch->XAddrs << ", "
                                  << probeMatch->Types << std::endl;
                    }
                }
            }
        } else if (onvif_soap.Error()) {
            break;
        }
    }

    SOAP_DBGLOG("\ndetect end! It has detected %d devices!\n", count);

    return;
}

int main(int argc, char **argv) {
    ONVIF_DetectDevice(nullptr);

    return 0;
}

执行结果

$ ./dev_scan 
http://10.10.10.103/onvif/device_service, tdn:NetworkVideoTransmitter tds:Device
http://10.10.10.104/onvif/device_service, tdn:NetworkVideoTransmitter tds:Device

detect end! It has detected 2 devices!

tdn:NetworkVideoTransmitter

NVT (Network Video Transmitter)


A Network Video Transmitter (NVT) is an ONVIF device that sends media data over
an IP network to a client. For example, an NVT may be an IP network camera or
an encoder device. 

An NVT implements the following services to provide its core functionality:

  • Device service enables an NVT to provide device management functionality such as device capabilities, system and network settings, security settings and firmware upgrade.
  • Event service enables an NVT to send events to clients. Media service enables an NVT to stream media data to clients. Media data includes video, audio, video analytics and other metadata.
  • Device IO service enables an NVT to support physical inputs and outputs. An NVT can also implement the following services to provide extended functionality:
  • PTZ service enables an NVT to provide PTZ control if the device is a PTZ camera.
  • Imaging service enables an NVT to provide configuration of image settings which affect the visual appearance of the video, for example, exposure time, gain and white balance, focus control.
  • Video Analytics service enables an NVT to provide video analytics functionality.

Beyond this, an NVT can also include additional ONVIF services, for example the Recording service if support for local storage is required

网络视频服务器(比如,网络摄像机,编码设备等),通过IP网络发送媒体数据到客户端。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值