Qt音視訊開發27-Onvif裝置搜尋

2020-10-05 12:01:05

一、前言

最近業餘時間主要研究音視訊開發這塊,前面的文章寫了好多種視訊監控核心,一旦將這些核心搞定以後,視訊監控的相關功能水到渠成。做視訊監控系統,繞不過onvif這玩意,這玩意主要就是為了統一一個大概的標準,能夠對各個廠家的監控裝置進行常用的一些操作,比如搜尋、獲取資訊、雲臺控制、事件訂閱、抓拍圖片等,如果沒有這個規範,那麼各個廠家都各自為政,需要用私有的sdk去處理,這樣就很麻煩很慘了,幾十個廠家就需要幾十個sdk,對於程式設計師來說簡直是災難,想想就很恐怖的事情,哪個程式設計師不想多活幾年!

onvif裝置搜尋是最基本的功能,想要對裝置進行進一步的處理,必須先搜尋到裝置,預設onvif搜尋只能搜尋到同一個網段的裝置,要跨網段的話,需要手動指定裝置的IP地址或者onvif地址進行搜尋,這兩者在封裝的onvif類中都考慮到了,經歷過各種複雜的現場情況的考驗,也可以算是本系統的一個小特色吧。

近期又重新把獨創的方法實現的onvif工具重新重構了下,各個類之間非常清晰明瞭,增強了相容性和完整性,在之前的基礎上還增加了很多基礎的處理比如視訊引數和圖片引數的獲取,設定時間等,同時還增加了可以指定過濾條件對搜尋的裝置進行過濾,這個非常有用,很多時候現場各種型別的各個廠家的攝像機非常多,一般來說一個型別的攝像機對應的onvif地址基本一致,埠也是一致,這樣可以指定格式進行過濾,只顯示過濾後的裝置。還增加了搜尋間隔,經過現場無數次的測試各種廠家,發現搜尋命令可能要發好幾種好幾次,以便所有裝置都能搜尋到,畢竟搜尋採用廣播的UDP,意味著可能丟包。

onvif主要的功能:

  1. 搜尋裝置,獲取裝置的資訊比如廠家、型號等。
  2. 獲取裝置的多個組態檔資訊profile。
  3. 獲取對應組態檔的視訊流地址rtsp,以及解析度等引數。
  4. 雲臺控制,上下左右移動,焦距放大縮小,相對和絕對移動。
  5. 獲取預置位資訊,觸發預置位。
  6. 訂閱事件,接收裝置的各種訊息尤其是報警事件比如IO口的報警。
  7. 抓圖,獲取裝置當前的圖片。
  8. 獲取、建立、刪除使用者資訊。
  9. 獲取和裝置網路設定資訊比如IP地址等。
  10. 獲取和設定NTP時間同步以及設定裝置時間。
  11. 獲取和設定視訊引數和圖片引數(亮度、色彩、飽和度)。
  12. 重新啟動裝置。

onvif的處理流程:

  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.14.2。
  8. 支援任意編譯器,親測mingw、msvc、gcc、clang。
  9. 支援任意作業系統,親測xp、win7、win10、android、linux、嵌入式linux、樹莓派全志H3等。
  10. 支援任意Onvif攝像機和NVR,親測海康、大華、宇視、華為、海思晶片核心等,可客製化開發。
  11. 支援對指定IP地址及onvif地址進行單播搜尋,比如跨網段情況下非常有用。
  12. 支援指定過濾條件過濾搜尋裝置。
  13. 支援搜尋間隔設定,保證所有裝置搜尋回來,在大量裝置現場很有用。
  14. 可對圖片引數(亮度、色彩度、飽和度)進行設定。
  15. 支援NTP校時和時間同步設定。
  16. 純Qt編寫,超級小巧輕量,總共約2000行程式碼,不依賴任何第三方的庫和元件,跨平臺。
  17. 封裝好了通用的資料傳送和接收解析的函數,可以非常方便的自行拓展其他Onvif處理。
  18. 工具上提供了收發資料文字方塊,顯示收發的資料,方便檢視和分析。
  19. 支援所有Onvif裝置,程式碼工整,介面友好,直接引入pri即可使用。

三、效果圖

在這裡插入圖片描述

四、相關站點

  1. 國內站點:https://gitee.com/feiyangqingyun/QWidgetDemo
  2. 國際站點:https://github.com/feiyangqingyun/QWidgetDemo
  3. 個人主頁:https://blog.csdn.net/feiyangqingyun
  4. 知乎主頁:https://www.zhihu.com/people/feiyangqingyun/
  5. 體驗地址:https://blog.csdn.net/feiyangqingyun/article/details/97565652

五、核心程式碼

void OnvifSearch::sendData()
{
    //依次傳送資料,如果到了最後一個則停止
    //根據onvif device test工具抓包分析,只要傳送前面兩個就行,後面兩個是ONVIF Device Manager抓包的
    //在收到結果的地方要對重複的進行過濾,因為部分裝置兩種協定請求都會返回
    //可以自行調整 timerSearch->stop(); 的位置來提高速度,比如很多情況下只要傳送一次就可以
    writeData("239.255.255.250");
    if (currentFile == ":/send/searchDevice1.xml") {
        currentFile = ":/send/searchDevice2.xml";
    } else if (currentFile == ":/send/searchDevice2.xml") {
        currentFile = ":/send/searchDevice3.xml";
    } else if (currentFile == ":/send/searchDevice3.xml") {
        currentFile = ":/send/searchDevice4.xml";
    } else if (currentFile == ":/send/searchDevice4.xml") {
        timerSearch->stop();
    }
}

void OnvifSearch::writeData(const QString &ip)
{
    QByteArray data = OnvifHelper::getFile(currentFile);
    if (!data.isEmpty()) {
        data = QString(data).arg(OnvifHelper::getUuid()).toUtf8();
        udpSocket->writeDatagram(data, QHostAddress(ip), 3702);
        emit sendData(data);
    }
}

void OnvifSearch::readData()
{
    QByteArray data;
    QHostAddress host;
    quint16 port = 0;
    while (udpSocket->hasPendingDatagrams()) {
        data.resize(udpSocket->pendingDatagramSize());
        udpSocket->readDatagram(data.data(), data.size(), &host, &port);
        QString ip = host.toString();
        checkData(data, ip);
        emit receiveData(data);
        //qDebug() << TIMEMS << ip << port << data.size();
    }
}

void OnvifSearch::readData(const QString &file)
{
    //從檔案讀取資料解析,主要方便用來測試各種攝像機返回的資料
    QFile f(file);
    if (f.open(QFile::ReadOnly)) {
        QByteArray data = f.readAll();
        data = data.replace("\\\"", "\"");
        checkData(data, "");
    }
}

void OnvifSearch::checkData(const QByteArray &data, const QString &ip)
{
    OnvifQuery query;
    query.setData(data);

    QString discovery = query.getDiscovery();
    QString addr_path = QString("//%1:ProbeMatches/%1:ProbeMatch/%1:XAddrs").arg(discovery);
    QString scopes_path = QString("//%1:ProbeMatches/%1:ProbeMatch/%1:Scopes").arg(discovery);
    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
        //發現還有串資料的要過濾 http://192.168.10.152/onvif/device_service http://192.168.10.172/onvif/device_service http://[fe80::9a8b:aff:fe4a:ad]/onvif/device_service
        QStringList list = addr.split(" ");
        if (ip.isEmpty()) {
            addr = list.first();
        } else {
            foreach (QString str, list) {
                if (str.contains(ip)) {
                    addr = str;
                    break;
                }
            }
        }

        //已經存在的地址不用繼續
        if (checkExist(addr)) {
            return;
        }

        //按照過濾條件過濾裝置 適用於裝置很多而且裝置型別很多的情況
        if (searchFilter != "none") {
            //預設80埠的不會帶 :80  前面有 http: https: 所以要取後面的 : 索引位置來判斷
            if (searchFilter == ":80") {
                if (addr.indexOf(":", 8) >= 8) {
                    return;
                }
            } else {
                if (!addr.contains(searchFilter)) {
                    return;
                }
            }
        }

        //定義結構體儲存裝置資訊
        DeviceInfo deviceInfo;
        deviceInfo.addr = addr;
        deviceInfo.ip = OnvifHelper::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));
    }
}
<?xml version="1.0" encoding="utf-8"?>
<Envelope xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns="http://www.w3.org/2003/05/soap-envelope">
  <Header>
    <wsa:MessageID xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">uuid:%1</wsa:MessageID>
    <wsa:To xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To>
    <wsa:Action xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</wsa:Action>
  </Header>
  <Body>
    <Probe xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.xmlsoap.org/ws/2005/04/discovery">
      <Types>tds:Device</Types>
      <Scopes />
    </Probe>
  </Body>
</Envelope>