一、前言

这个工具很早以前大概在2013年就想做了,后面杂七杂八的事情一再耽搁,记得当时最初用的是soap类来搜索和解析的,后面发现太大了,每次编译都要等好久,光源码文件加起来都快10MB了,而且函数名非常另类,大量的下划线等,反正本人非常不适应,近期经过一个朋友(QQ:408815041)的前期探索,对整个处理流程熟悉以后,发现其实用纯Qt也可以实现,核心就是udp搜索+post数据。
本程序框架的最大难点在找出对应的数据以及节点数据解析。找出对应的数据可以直接使用官方的ONVIF Device Test Tool,抓包即可。数据解析一开始采用xml的节点解析,发现根本行不通,因为返回的数据不是标准的xml数据,而是soap格式的数据,需要用QXmlQuery来解析。本程序只实现了设备信息的搜索和云台控制,并未实现服务端,服务端一般是IPC或者NVR上来实现。
体验地址:https://pan.baidu.com/s/1bbL2ZughZAgfIGrexyN-9g 提取码:zkeh,下面的bin_onviftool.zip,如果是XP系统,请先执行目录下的fixff.cmd。

二、Onvif介绍

ONVIF致力于通过全球性的开放接口标准来推进网络视频在安防市场的应用,这一接口标准将确保不同厂商生产的网络视频产品具有互通性。2008年11月,正式发布了ONVIF第一版规范——ONVIF核心规范1.0。随着视频监控的网络化应用,产业链的分工将越来越细。有些厂商专门做摄像头,有些厂商专门做DVS,有些厂商则可能专门做平台等,然后通过集成商进行集成,提供给最终客户。这种产业合作模式,已经迫切的需要行业提供越来越标准化的接口平台。
ONVIF规范描述了网络视频的模型、接口、数据类型以及数据交互的模式。并复用了一些现有的标准,如WS系列标准等。ONVIF规范的目标是实现一个网络视频框架协议,使不同厂商所生产的网络视频产品(包括摄录前端、录像设备等)完全互通。
ONVIF规范中设备管理和控制部分所定义的接口均以Web Services的形式提供,设备作为服务提供者为服务端。ONVIF规范涵盖了完全的XML及WSDL的定义。每一个支持ONVIF规范的终端设备均须提供与功能相应的Web Service。服务端与客户端的数据交互采用SOAP协议。ONVIF中的其他部分比如音视频流则通过RTP/RTSP进行。

三、处理流程

  1. 绑定组播IP(239.255.255.250)和端口(3702),发送固定的xml格式的数据搜索设备。
  2. 接收到的xml格式的数据解析,得到设备的Onvif地址。
  3. 对Onvif地址发送对应的数据,收到数据取出对应的节点数据。
  4. 请求Onvif地址获取Media地址和Ptz地址,Media地址用来获取详细的配置文件,Ptz地址用来云台控制。
  5. ptz控制是对Ptz地址发送对应的数据即可。
  6. 设置了用户认证的需要组织用户token信息一块发送,每次都需要作鉴权处理。
  7. 接收到的数据不是标准的xml数据,没法按照正常的节点解析来处理,只能用QXmlQuery来做。
  8. 每个厂家设备返回的数据未必完全一致,基本上都不一致,需要进行模糊查找节点值。
  9. 特意采用底层协议解析,因为soap太臃肿函数名称太另类,特意做的轻量级的。
  10. 两个必备工具,Onvif Device Manager 和 Onvif Device Test Tool。

四、功能特点

  1. 广播搜索设备,支持IPC和NVR,依次返回,可选择不同的网卡IP。
  2. 依次获取Onvif地址、Media地址、Profile文件、Rtsp地址。
  3. 可对指定的Profile获取视频流Rtsp地址,比如主码流子码流地址。
  4. 可对每个设备设置Onvif用户信息,用于认证获取详细信息。
  5. 可实时预览摄像机图像。
  6. 支持云台控制,可上下左右调节云台,支持绝对移动和相对移动,可放到和缩小图像远近。
  7. 支持Qt4和Qt5任意Qt版本,亲测Qt4.7.0到Qt5.12.4。
  8. 支持任意编译器,亲测mingw、msvc、gcc、clang。
  9. 支持任意操作系统,亲测xp、win7、win10、linux、嵌入式linux、树莓派全志H3等。
  10. 支持任意Onvif摄像机和NVR,亲测海康、大华、宇视、华为、海思芯片内核等,可定制开发。
  11. 支持对指定IP地址进行单播搜索,比如跨网段情况下非常有用。
  12. 纯Qt编写,超级小巧轻量,总共约2000行代码,不依赖任何第三方的库和组件,跨平台。
  13. 封装好了通用的数据发送和接收解析的函数,可以非常方便的自行拓展其他Onvif处理比如修改IP等。
  14. 工具上提供了收发数据文本框,显示收发的数据,方便查看和分析。
  15. 支持所有Onvif设备,代码工整,接口友好,直接引入pri即可使用。

五、效果图

Qt编写Onvif搜索及云台控制工具_QT教程

六、核心代码

#include "qonvifsearch.h"
#include "qonviffunction.h"
#include "qonvifquery.h"

//onvif协议固定的IP和端口
#define OnvifAddr QHostAddress("239.255.255.250")
#define OnvifPort 3702

QOnvifSearch::QOnvifSearch(QObject *parent) : QObject(parent)
{
    isOk = false;
    //定时器排队发送搜索命令,有好几种
    timer = new QTimer(this);
    connect(timer, SIGNAL(timeout()), this, SLOT(sendData()));
    timer->setInterval(300);

    udpSocket = new QUdpSocket(this);
#if (QT_VERSION >= QT_VERSION_CHECK(4,8,5))
    udpSocket->setSocketOption(QAbstractSocket::MulticastLoopbackOption, 1);
#endif
    connect(udpSocket, SIGNAL(readyRead()), this, SLOT(readData()));
}

QOnvifSearch::~QOnvifSearch()
{
    if (timer->isActive()) {
        timer->stop();
    }
}

void QOnvifSearch::sendData()
{
    QByteArray data = QOnvifFunction::getFile(currentFile);
    if(!data.isEmpty()) {
        data = QString(data).arg(QOnvifFunction::getUuid()).toUtf8();
        udpSocket->writeDatagram(data, OnvifAddr, OnvifPort);
        emit sendData(data);
    }

    //依次发送数据,如果到了最后一个则停止
    //根据onvif device test工具抓包分析,只要发送前面两个就行,后面两个是ONVIF Device Manager抓包的
    //在收到结果的地方要对重复的进行过滤,因为部分设备两种协议请求都会返回
    if (currentFile == ":/send/search1.xml") {
        currentFile = ":/send/search2.xml";
    } else if (currentFile == ":/send/search2.xml") {
        currentFile = ":/send/search3.xml";
    } else if (currentFile == ":/send/search3.xml") {
        currentFile = ":/send/search4.xml";
    } else if (currentFile == ":/send/search4.xml") {
        timer->stop();
    }
}

void QOnvifSearch::readData()
{
    QByteArray data;
    QHostAddress host;
    quint16 port = 0;
    while (udpSocket->hasPendingDatagrams()) {
        data.resize(udpSocket->pendingDatagramSize());
        udpSocket->readDatagram(data.data(), data.size(), &host, &port);
        emit receiveData(data);
    }

    QOnvifQuery query;
    query.setData(data);

    QString addr_path = QString("//%1:ProbeMatches/%1:ProbeMatch/%1:XAddrs").arg(query.getDiscovery());
    QString scopes_path = QString("//%1:ProbeMatches/%1:ProbeMatch/%1:Scopes").arg(query.getDiscovery());
    QString addr = query.getValue(addr_path);
    QString scopes = query.getValue(scopes_path);

    if(!addr.isEmpty()) {
        //过滤下IPV6地址 http://192.168.1.64/onvif/device_service http://[fe80::9a8b:aff:fe6e:867c]/onvif/device_service
        QStringList list = addr.split(" ");
        addr = list.first();

        //过滤掉重复的设备,发送搜索设备的命令有好几种,某些设备支持多种命令,所以会返回多次
        foreach (DeviceInfo deviceInfo, deviceInfos) {
            if (deviceInfo.addr == addr) {
                return;
            }
        }

        //定义结构体存储设备信息
        DeviceInfo deviceInfo;
        deviceInfo.addr = addr;
        deviceInfo.ip = QOnvifFunction::getIP(addr);

        //取出其他信息 onvif://www.onvif.org/type/NetworkVideoTransmitter onvif://www.onvif.org/name/NVR onvif://www.onvif.org/hardware/hisi onvif://www.onvif.org/location/shanghai
        //这里的信息是通过广播搜索返回的无需密码,这里还可以根据打印出来的 scopes 自行增加设备信息
        list = scopes.split(" ");
        foreach (QString str, list) {
            QStringList l = str.split("/");
            if (l.contains("name")) {
                deviceInfo.name = l.last();
            } else if (l.contains("location")) {
                deviceInfo.location = l.last();
            } else if (l.contains("hardware")) {
                deviceInfo.hardware = l.last();
            }
        }

        deviceInfos << deviceInfo;
        emit receiveDevice(deviceInfo);
        emit receiveInfo(QString("发现新设备-> %1").arg(addr));
    }
}

bool QOnvifSearch::search(const QString &ip)
{
    deviceInfos.clear();
    if (!QOnvifFunction::isIP(ip)) {
        return false;
    }

    //如果还未成功则先绑定
    if (!isOk) {
        isOk = udpSocket->bind(QHostAddress(ip), 0, QUdpSocket::ShareAddress);
        //udpSocket->joinMulticastGroup(OnvifAddr);
    }

    if (isOk) {
        //之前是直接全部放在这里发送,发现部分设备要好几次才能回来
        //改成定时器排队发送多种广播搜索数据,就没有问题
        currentFile = ":/send/search1.xml";
        timer->stop();
        timer->start();
    } else {
        emit receiveError(QString("绑定组播失败-> %1").arg(udpSocket->errorString()));
    }

    return isOk;
}

QList<QOnvifSearch::DeviceInfo> QOnvifSearch::getDeviceInfos()
{
    return this->deviceInfos;
}

QStringList QOnvifSearch::getAddrs()
{
    QStringList addrs;
    foreach (DeviceInfo deviceInfo, deviceInfos) {
        addrs << deviceInfo.addr;
    }

    return addrs;
}