一、前言
這個工具很早以前大概在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進行。
三、處理流程
- 綁定組播IP(239.255.255.250)和端口(3702),發送固定的xml格式的數據搜索設備。
- 接收到的xml格式的數據解析,得到設備的Onvif地址。
- 對Onvif地址發送對應的數據,收到數據取出對應的節點數據。
- 請求Onvif地址獲取Media地址和Ptz地址,Media地址用來獲取詳細的配置文件,Ptz地址用來雲台控制。
- ptz控制是對Ptz地址發送對應的數據即可。
- 設置了用戶認證的需要組織用戶token信息一塊發送,每次都需要作鑒權處理。
- 接收到的數據不是標准的xml數據,沒法按照正常的節點解析來處理,只能用QXmlQuery來做。
- 每個廠家設備返回的數據未必完全一致,基本上都不一致,需要進行模糊查找節點值。
- 特意采用底層協議解析,因為soap太臃腫函數名稱太另類,特意做的輕量級的。
- 兩個必備工具,Onvif Device Manager 和 Onvif Device Test Tool。
四、功能特點
- 廣播搜索設備,支持IPC和NVR,依次返回,可選擇不同的網卡IP。
- 依次獲取Onvif地址、Media地址、Profile文件、Rtsp地址。
- 可對指定的Profile獲取視頻流Rtsp地址,比如主碼流子碼流地址。
- 可對每個設備設置Onvif用戶信息,用於認證獲取詳細信息。
- 可實時預覽攝像機圖像。
- 支持雲台控制,可上下左右調節雲台,支持絕對移動和相對移動,可放到和縮小圖像遠近。
- 支持Qt4和Qt5任意Qt版本,親測Qt4.7.0到Qt5.12.4。
- 支持任意編譯器,親測mingw、msvc、gcc、clang。
- 支持任意操作系統,親測xp、win7、win10、linux、嵌入式linux、樹莓派全志H3等。
- 支持任意Onvif攝像機和NVR,親測海康、大華、宇視、華為、海思芯片內核等,可定制開發。
- 支持對指定IP地址進行單播搜索,比如跨網段情況下非常有用。
- 純Qt編寫,超級小巧輕量,總共約2000行代碼,不依賴任何第三方的庫和組件,跨平台。
- 封裝好了通用的數據發送和接收解析的函數,可以非常方便的自行拓展其他Onvif處理比如修改IP等。
- 工具上提供了收發數據文本框,顯示收發的數據,方便查看和分析。
- 支持所有Onvif設備,代碼工整,接口友好,直接引入pri即可使用。
五、效果圖
六、核心代碼
#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;
}