ONVIF協議網絡攝像機(IPC)客戶端程序開發(15):遮擋報警

版權聲明:本文爲博主原創文章,如要轉載請標明出處。 https://blog.csdn.net/benkaoya/article/details/78084227

1 專欄導讀

本專欄第一篇文章「專欄開篇」列出了專欄的完整目錄,按目錄順序閱讀,有助於你的理解,專欄前面文章講過的知識點(或代碼段),後面文章不會贅述。爲了節省篇幅,突出重點,在文章中展示的示例代碼僅僅是關鍵代碼,你可以在「專欄開篇」中獲取完整代碼。

如有錯誤,歡迎你的留言糾正!讓我們共同成長!你的「點贊」「打賞」是對我最大的支持和鼓勵!

2 原理簡介

IPC攝像頭往往帶有告警功能,如移動偵測、遮擋報警等,這些告警會被描述爲事件傳給客戶端,客戶端再對各類事件分析處理併產生相應聯動(如郵件通知、上傳中心等)。本文將以“遮擋報警”功能爲例,講解ONVIF客戶端如何檢測IPC攝像頭的告警功能。

在「ONVIF Core Specification」規格說明書中(注:書稿時我參考的是「ONVIF-Core-Specification-v1612」版本,本文以下內容如果沒有特別說明,ONVIF Core說的都是這個版本),其中一個章節「Event handling」規範了ONVIF事件處理。在開始之前,建議你先閱讀下這部分規範。

如果你對英文「過敏」,可以參考基於2013版「ONVIF-Core-Specification-v230」的中文翻譯:https://github.com/jimxl/onvif-core-specification-cn.git

ONVIF的事件處理標準並不是自己定義的,而是使用現成的OASIS的「WS-BaseNotification」和「WS-Topics」規範。WS-BaseNotification規範定義了消息訂閱和通知操作、信息交互過程中的術語、概念、操作和交互語言的格式等,也定義了生產者和消費者的角色。WS-Topics規範定義了主題的概念來對事件進行組織和分類,並定義了對事件進行篩選的語法和表達式。有關這方面的規範,不在本文討論範圍內,你可以通過「WS-Notification」關鍵字在網上進行查閱瞭解更新信息。

總的來說,根據ONVIF的事件處理規範,我們可以:

  • 客戶端通過ONVIF接口向IPC攝像頭訂閱感興趣的事件主題。
  • 一旦告警發生,IPC攝像頭就會告警描述爲事件消息通知客戶端。

根據ONVIF Core Specification規範,實現事件通知的方式有以下三種:

  • Basic Notification Interface
    基本通知接口。這種方式要求IPC攝像機和客戶端必須在同一網段,如果不在同一網段,事件通知消息將無法傳輸。這種方式的事件通知消息也無法穿越防火牆,這就要求即使是在生產階段,用戶也得關閉任何可能存在的防火牆機制。正因爲存在諸多限制,這種方式在實際中很少被使用。

  • Real-time Pull-Point Notification Interface
    實時拉點通知接口。因爲更好的防火牆穿透能力,Pull-Point方式更受推薦,基本上所有的IPC攝像機供應商都支持這種方式。這種方式是本文重點講解的內容。

  • Notification Streaming Interface
    通知流接口。這種方式將事件消息通過RTP流數據包的方式通知客戶端,但是書稿時,很少有供應商支持這種方式,本文將不做介紹。

2.1 Basic Notification

根據WS-BaseNotification規範,ONVIF的事件處理機制定義了三種角色,即客戶端、事件服務器和訂閱管理器。

  • 客戶端(Client):實現NotificationConsumer接口。
  • 事件服務(Event Service):實現NotificationProducer接口。
  • 訂閱管理器(Subscription Manager):實現BaseSubscriptionManager接口。



圖1 Basic Notification序列圖

Basic Notification方式的工作流程如上圖所示:

  1. 事件服務和訂閱管理器都是在設備(IPC攝像頭)上實現的。
  2. 客戶端建立一個連接到事件服務,並通過SubscriptionRequest訂閱感興趣的事件主題。
  3. 如果事件服務接受訂閱,它會動態實例化一個SubscriptionManager來表示訂閱,事件服務會在SubscriptionResponse應答中返回SubscriptionManager地址)。
  4. 爲了傳送與訂閱相匹配的通知,需要另外建立一個從事件服務到客戶端的連接。通過此連接,事件服務發送一個單向通知消息到客戶端的NotificationConsumer接口。由於告警通知隨時可能產生,所以額外的這條連接必須保持在線。
  5. 在SubscriptionRequest中,客戶端可以指定一個終止時間,一旦超時SubscriptionManager會自動銷燬。客戶端也可以通過SubscriptionResponse應答中的SubscriptionManager地址對訂閱進行控制,如使用RenewRequests續訂,使用UnsubscribeRequest退訂(明確終止SubscriptionManager)。

我試圖使用這種方法來檢測遮擋報警,但未能成功。我使用gSOAP自動生成的函數soap_recv___tev__Notify來接收IPC攝像頭的消息,但soap_recv___tev__Notify函數一直處於阻塞狀態,不懂是IPC攝像頭不支持這種方式,還是我的代碼有問題。

2.2 Pull-Point Notification



圖2 Real-time Pull-Point Notification序列圖

Pull-Point方式的工作流程如上圖所示:

  1. 事件服務和訂閱管理器(PullPoint)都是在設備(IPC攝像頭)上實現的。
  2. 客戶端使用 CreatePullPointSubscriptionRequest 向事件服務訂閱感興趣的事件主題。
  3. 如果事件服務接受訂閱,它會動態實例化一個PullPoint來表示訂閱,事件服務會在CreatePullPointSubscriptionResponse應答中返回PullPoint地址,這個地址在後續的PullMessages操作會用到。設備可以支持多個pull points,可以通過ONVIF接口GetServiceCapabilities查詢到MaxPullPoints值。
  4. 客戶端通過PullMessages向PullPoint拉取消息。當跟訂閱相匹配的事件發生時,立即通過PullMessagesRequest應答向客戶端返回事件描述;如果在指定時間內未發生事件,則超時返回(不同IPC攝像頭廠家的超時時間單位不同)。客戶端在接收到應答後可立即發起新的PullMessages請求。
  5. 最後客戶端通過UnSubscribeRequest退訂事件主題。

跟Pull-Point方式相比,Basic Notification方式需要多創建一個連接,這是有原因的。Event上報事件可以分爲兩種pull和push。

  • Pull採用client定時發送獲取告警的消息,即輪詢方式,device如果有告警則上報,將notification中填入告警的信息,client根據notification中的信息顯示告警。Pull-Point方式屬於Pull。

  • Push應該是採用device一旦有告警發生,則通過額外的連接主動上報給client,client實時監聽device的上報信息。所以,push實時性應該更好,pull模式的實時性差一點。Basic Notification方式屬於Push。

3 啓用遮擋報警

要測試遮擋報警,得先啓用遮擋報警功能,可以通過web登錄IPC後臺進行配置,如下圖所示。那能不能用ONVIF接口去開啓/關閉這個功能呢,還有待研究。



圖3 大華IPC

除了得啓用之外,有的IPC還要求繪製區域,否則還用不了遮擋報警功能,如海康:



圖4 海康IPC

4 重新生成ONVIF代碼

爲了檢測遮擋報警,ONVIF代碼得加入Event模塊。本專欄前面的ONVIF代碼沒有加入Event模塊,所以得重新生成ONVIF代碼。如何使用gSOAP工具生成ONVIF框架代碼,可以參考之前的一篇文章:

ONVIF協議網絡攝像機(IPC)客戶端程序開發(6):使用gSOAP生成ONVIF框架代碼

以下簡要說明步驟:

(1). 參考gSOAP官網說明修改gsoap\typemap.dat

參考「How do I use gSOAP with the ONVIF specifications」說明,看是否需要修改typemap.dat。我用的gSOAP工具版本是gsoap_2.8.45,typemap.dat文件剛好符合要求,不用改。

(2). 爲了支持Events模塊,在gsoap\typemap.dat文件末尾加上

# 解決:PullMessages收不到事件通知
_wsnt__NotificationMessageHolderType_Message = $ struct _tt__Message* tt__Message;

# 解決:CreatePullPointSubscription無法訂閱感興趣的主題
wsnt__FilterType = $ struct wsnt__TopicExpressionType* TopicExpression;

# 解決:GetEventProperties無法解析TopicSet字段
wstop__TopicSetType = $ _XML __mixed;

爲什麼要加這幾行,詳情見「爲什麼typemap.dat要加幾行」章節的說明,這幾行可是折騰了我好長時間。

(3). 使用wsdl2h工具,根據WSDL產生頭文件

# cd gsoap-2.8/gsoap/
# mkdir -p samples/onvif
# wsdl2h -P -x -c -s -t ./typemap.dat -o samples/onvif/onvif.h https://www.onvif.org/ver10/network/wsdl/remotediscovery.wsdl https://www.onvif.org/ver10/device/wsdl/devicemgmt.wsdl https://www.onvif.org/ver10/media/wsdl/media.wsdl https://www.onvif.org/ver10/events/wsdl/event.wsdl

(4). 因「授權」需要,修改onvif.h頭文件

有些ONVIF接口調用時需要攜帶認證信息,要使用soap_wsse_add_UsernameTokenDigest函數進行授權,所以要在onvif.h頭文件開頭加入

#import "wsse.h"

(5). 使用soapcpp2工具,根據頭文件產生框架代碼

# soapcpp2 -2 -C -L -c -x -I import:custom -d samples/onvif/ samples/onvif/onvif.h

如果出現以下錯誤:

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

解決方法:

修改import\wsa5.h文件,將int SOAP_ENV__Fault修改爲int SOAP_ENV__Fault_alex,再次使用soapcpp2工具編譯就成功了。

(6). 拷貝其他還有會用的源碼

# cp stdsoap2.c stdsoap2.h dom.c plugin/wsaapi.c plugin/wsaapi.h custom/duration.c custom/duration.h plugin/mecevp.c plugin/mecevp.h plugin/smdevp.c plugin/smdevp.h plugin/threads.c plugin/threads.h  plugin/wsseapi.c plugin/wsseapi.h samples/onvif/

(7). 關聯自己的命名空間,修改stdsoap2.c文件

在samples\onvif\stdsoap2.h中有命名空間「namespaces變量」的定義聲明,如下所示:

extern SOAP_NMAC struct Namespace namespaces[];

但「namespaces變量」的定義實現,是在samples\onvif\wsdd.nsmap文件中,爲了後續應用程序要順利編譯,修改samples\onvif\stdsoap2.c文件,在開頭加入:

#include "wsdd.nsmap"

當然,你可以在其他源碼中(更上層的應用程序源碼)include,我這裏是選擇在stdsoap2.c中include的。

5 編碼流程

本文只介紹Pull-Point Notification方式檢測IPC遮擋報警,其他方式不做介紹。Pull-Point Notification的編碼流程在前面的系列圖中已經介紹過了。

6 示例代碼

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "onvif_comm.h"
#include "onvif_dump.h"

#ifdef WIN32
#include <windows.h>
#endif

/* 遮擋報警 */
#define TAMPER_TOPIC            "tns1:RuleEngine/TamperDetector/Tamper"
#define TAMPER_NAME             "IsTamper"
#define TAMPER_VALUE            "true"

/************************************************************************
**函數:find_event
**功能:查找指定主題、指定內容的事件
**參數:略
**返回:
        0表明未找到,非0表明找到
************************************************************************/
int find_event(struct _tev__PullMessagesResponse *rep, char *topic, char *name, char *value)
{
    int i, j;

    if(NULL == rep) {
        return 0;
    }

    for (i = 0; i < rep->__sizeNotificationMessage; i++) {
        struct wsnt__NotificationMessageHolderType *p = rep->wsnt__NotificationMessage + i;

        if (NULL == p->Topic) {
            continue;
        }
        if (NULL == p->Topic->__mixed ) {
            continue;
        }
        if (0 != strcmp(topic, p->Topic->__mixed)) {
            continue;
        }

        if (NULL == p->Message.tt__Message) {
            continue;
        }
        if (NULL == p->Message.tt__Message->Data) {
            continue;
        }
        if (NULL == p->Message.tt__Message->Data->SimpleItem) {
            continue;
        }
        for (j = 0; j < p->Message.tt__Message->Data->__sizeSimpleItem; j++) {
            struct _tt__ItemList_SimpleItem *a = p->Message.tt__Message->Data->SimpleItem + j;
            if (NULL == a->Name || NULL == a->Value) {
                continue;
            }
            if (0 != strcmp(name, a->Name)) {
                continue;
            }
            if (0 != strcmp(value, a->Value)) {
                continue;
            }

            return 1;
        }
    }


    return 0;
}

/************************************************************************
**函數:IsTamper
**功能:判斷是否有遮擋報警
**參數:略
**返回:
        0表明沒有,非0表明有
************************************************************************/
int IsTamper(struct _tev__PullMessagesResponse *rep)
{
    return find_event(rep, TAMPER_TOPIC, TAMPER_NAME, TAMPER_VALUE);
}

/************************************************************************
**函數:ONVIF_CreatePullPointSubscription
**功能:使用Pull-Point方式訂閱事件
**參數:
        [in] EventXAddr - 事件服務地址
**返回:
        0表明成功,非0表明失敗
************************************************************************/
int ONVIF_CreatePullPointSubscription(const char *EventXAddr)
{
    int i;
    int result = 0;
    struct soap *soap = NULL;
    char *pullpoint = NULL;
    struct _tev__CreatePullPointSubscription         req;
    struct _tev__CreatePullPointSubscriptionResponse rep;

    struct _tev__PullMessages                        req_pm;
    struct _tev__PullMessagesResponse                rep_pm;

    struct _wsnt__Unsubscribe                        req_u;
    struct _wsnt__UnsubscribeResponse                rep_u;

#if 0
    #define PULLMSG_TIMEOUT_UNIT    (1000)                                      // 海康IPC單位
#else
    #define PULLMSG_TIMEOUT_UNIT    (5000000)                                   // 大華IPC單位
#endif

    LONG64 pullmsg_timeout = 5 * PULLMSG_TIMEOUT_UNIT;                          // PullMessages查詢事件的超時時間,不同IPC廠家的單位不同
    int socket_timeout = 10;                                                    // 創建soap的socket超時時間,單位秒

    SOAP_ASSERT(pullmsg_timeout < socket_timeout * PULLMSG_TIMEOUT_UNIT);       // 要確保查詢事件的超時時間比socket超時時間小,否則,事件沒查詢到就socket超時,導致PullMessages返回失敗
    SOAP_ASSERT(NULL != EventXAddr);
    SOAP_ASSERT(NULL != (soap = ONVIF_soap_new(socket_timeout)));

    /*
    *
    * 可以通過主題過濾我們所訂閱的事件,過濾規則在官方「ONVIF Core Specification」規格說明書「Topic Filter」章節裏有詳細的介紹。
    * 比如:
    * tns1:RuleEngine/TamperDetector/Tamper   只關心遮擋報警
    * tns1:RuleEngine/TamperDetector//.       只關心主題TamperDetector樹下的事件
    * NULL                                    關心所有事件,即不過濾
    *                                         也可以通過 '|' 表示或的關係,即同時關心某幾類事件
    *
    */
    memset(&req, 0x00, sizeof(req));                                            // 訂閱事件
    memset(&rep, 0x00, sizeof(rep));
    req.Filter = (struct wsnt__FilterType *)ONVIF_soap_malloc(soap, sizeof(struct wsnt__FilterType));
    req.Filter->TopicExpression = (struct wsnt__TopicExpressionType *)ONVIF_soap_malloc(soap, sizeof(struct wsnt__TopicExpressionType));
    req.Filter->TopicExpression->Dialect = "http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet";
    req.Filter->TopicExpression->__mixed = TAMPER_TOPIC;
    ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);
    result = soap_call___tev__CreatePullPointSubscription(soap, EventXAddr, NULL, &req, &rep);
    SOAP_CHECK_ERROR(result, soap, "CreatePullPointSubscription");
    dump_tev__CreatePullPointSubscriptionResponse(&rep);

    pullpoint = rep.SubscriptionReference.Address;                              // 提取pull point地址

    for (i = 0; i < 30; i++) {                                                  // 輪詢事件
        memset(&req_pm, 0x00, sizeof(req_pm));
        memset(&rep_pm, 0x00, sizeof(rep_pm));
        req_pm.Timeout      = pullmsg_timeout;
        req_pm.MessageLimit = 0;
        ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);
        result = soap_call___tev__PullMessages(soap, pullpoint, NULL, &req_pm, &rep_pm);
        SOAP_CHECK_ERROR(result, soap, "PullMessages");
        dump_tev__PullMessagesResponse(&rep_pm);

        if(IsTamper(&rep_pm)) {                                                 // 是遮擋報警?
            SOAP_DBGLOG("Tamper...\n");
        }
    }

EXIT:

    memset(&req_u, 0x00, sizeof(req_u));                                        // 退訂事件
    memset(&rep_u, 0x00, sizeof(rep_u));
    ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);
    result = soap_call___tev__Unsubscribe(soap, pullpoint, NULL, &req_u, &rep_u);
    if (SOAP_OK != result || SOAP_OK != soap->error) {
        soap_perror(soap, "Unsubscribe");
        if (SOAP_OK == result) {
            result = soap->error;
        }
    }

    if (NULL != soap) {
        ONVIF_soap_delete(soap);
    }

    return result;
}

/************************************************************************
**函數:ONVIF_GetEventProperties
**功能:獲取事件屬性
**參數:
        [in] EventXAddr - 事件服務地址
**返回:
        0表明成功,非0表明失敗
************************************************************************/
int ONVIF_GetEventProperties(const char *EventXAddr)
{
    int result = 0;
    struct soap *soap = NULL;
    struct _tev__GetEventProperties         req;
    struct _tev__GetEventPropertiesResponse rep;

    SOAP_ASSERT(NULL != EventXAddr);
    SOAP_ASSERT(NULL != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));

    memset(&req, 0x00, sizeof(req));
    memset(&rep, 0x00, sizeof(rep));
    ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);
    result = soap_call___tev__GetEventProperties(soap, EventXAddr, NULL, &req, &rep);
    SOAP_CHECK_ERROR(result, soap, "GetEventProperties");
    dump_tev__GetEventPropertiesResponse(&rep);

EXIT:

    if (NULL != soap) {
        ONVIF_soap_delete(soap);
    }

    return result;
}

/************************************************************************
**函數:ONVIF_GetServiceCapabilities
**功能:獲取服務功能
**參數:
        [in] EventXAddr - 事件服務地址
**返回:
        0表明成功,非0表明失敗
************************************************************************/
int ONVIF_GetServiceCapabilities(const char *EventXAddr)
{
    int result = 0;
    struct soap *soap = NULL;
    struct _tev__GetServiceCapabilities         req;
    struct _tev__GetServiceCapabilitiesResponse rep;

    SOAP_ASSERT(NULL != EventXAddr);
    SOAP_ASSERT(NULL != (soap = ONVIF_soap_new(SOAP_SOCK_TIMEOUT)));

    memset(&req, 0x00, sizeof(req));
    memset(&rep, 0x00, sizeof(rep));
    ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);
    result = soap_call___tev__GetServiceCapabilities(soap, EventXAddr, NULL, &req, &rep);
    SOAP_CHECK_ERROR(result, soap, "GetServiceCapabilities");
    dump_tev__GetServiceCapabilitiesResponse(&rep);

EXIT:

    if (NULL != soap) {
        ONVIF_soap_delete(soap);
    }

    return result;
}

void cb_discovery(char *DeviceXAddr)
{
    struct tagCapabilities capa;

    ONVIF_GetCapabilities(DeviceXAddr, &capa);                                  // 獲取設備能力信息(獲取媒體服務地址)
    ONVIF_GetServiceCapabilities(capa.EventXAddr);                              // 獲取服務功能
    ONVIF_GetEventProperties(capa.EventXAddr);                                  // 獲取事件屬性
    ONVIF_CreatePullPointSubscription(capa.EventXAddr);                         // 使用Pull-Point方式訂閱事件
}

int main(int argc, char **argv)
{
    parse_options(argc, argv);

    if (NULL == g_deviceXAddr) {
        ONVIF_DetectDevice(cb_discovery);
    } else {
        cb_discovery(g_deviceXAddr);
    }

    return 0;
}

7 PullMessages超時時間

在上面的示例代碼中,soap_call___tev__PullMessages函數輸入參數有一個超時時間Timeout,如果在指定時間內未發生事件,函數則超時返回。Timeout所在的結構體如下所示,那Timeout單位是什麼,官方沒有明確說明(或許是我沒找到)。

struct _tev__PullMessages {
        LONG64 Timeout;
        int MessageLimit;
};

我們先寫一個小測試程序測試下Timeout的單位,讓Timeout從0開始遞增,通過分析SOAP協議中的XML數據,觀察每次遞增會帶來什麼變化,測試程序主要代碼片段如下:

for (i = 0; i < 30; i++) {
    memset(&req_pm, 0x00, sizeof(req_pm));
    memset(&rep_pm, 0x00, sizeof(rep_pm));
    req_pm.Timeout = i;
    req_pm.MessageLimit = 0;
    ONVIF_SetAuthInfo(soap, USERNAME, PASSWORD);
    soap_call___tev__PullMessages(soap, pullpoint, NULL, &req_pm, &rep_pm);
}

我們可以通過Wireshark工具抓包SOAP協議數據,但還有更好的方法,就是編譯代碼時,加上-DDEBUG宏開啓SOAP協議收發日誌,程序運行後,SOAP協議數據會被寫入RECV.log、SENT.log、TEST.log文件。

我們分析的是PullMessages產生的SOAP協議數據,它的日誌會被保存在SENT.log中,用關鍵字「Timeout」搜索即可快速定位。從以下測試數據,很容易看出來,Timeout每次遞增,SOAP協議數據中的超時時間就會增加0.001秒,所以ONVIF標準裏規定的Timeout單位是毫秒。



圖5 測試PullMessages中Timeout單位

似乎我們找到答案了,但,但,但,通過實際測試,不同IPC攝像頭廠家的超時時間單位是不同的。海康IPC攝像頭確實按標準來,Timeout單位爲1毫秒,大華IPC攝像頭的Timeout單位是5微妙(經驗值,可能會有一些偏差,沒有得到大華官方資料的確認,也不知道是否精確)。如果設定的Timeout值超出IPC攝像頭能接受的範圍,會報如下錯誤:

[soap] PullMessages error: 12, SOAP-ENV:Sender, the parameter value is illegal

另外還需要注意的是,除了PullMessages接口自帶的Timeout超時時間之外,創建sock時還有一個tcp超時時間,如果tcp超時時間先起作用,會導致PullMessages函數返回失敗,並且連接被斷開,導致後續的PullMessages調用馬上失敗。所以,最好保證Timeout小於tcp超時時間,以避免邏輯上的錯誤。

8 爲什麼typemap.dat要加幾行

在前面的「重新生成ONVIF代碼」章節中,爲了支持Events模塊,在執行wsdl2h命令之前,得在gsoap\typemap.dat文件末尾加上:

# 解決:PullMessages收不到事件通知
_wsnt__NotificationMessageHolderType_Message = $ struct _tt__Message* tt__Message;

# 解決:CreatePullPointSubscription無法訂閱感興趣的主題
wsnt__FilterType = $ struct wsnt__TopicExpressionType* TopicExpression;

# 解決:GetEventProperties無法解析TopicSet字段
wstop__TopicSetType = $ _XML __mixed;

這幾行可是折騰了我好長時間,很多初學者搞不懂爲什麼要加這幾行,這裏做個詳細解釋。

8.1 PullMessages爲何收不到事件通知

剛開始,我的demo程序通過PullMessages接口一直收不到IPC攝像頭的事件通知。

爲了解釋清楚,我們先看看PullMessagesResponse應答消息是長什麼樣的,裏面都有哪一些信息。「ONVIF Core Specification」規格說明書中,在Event handling > Notification example > PullMessagesResponse章節,給出了PullMessagesResponse應答XML消息樣例,如下所示。如果你想看自己手上IPC攝像頭的PullMessagesResponse應答XML消息,你可以通過ONVIF Device Test Tool 工具或者Wireshark等網絡抓包工具抓包查看。

<?xml version="1.0" encoding="UTF-8"?>
  <SOAP-ENV:Envelope
    xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope"
    xmlns:wsa="http://www.w3.org/2005/08/addressing"
    xmlns:wstop="http://docs.oasis-open.org/wsn/t-1"
    xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2"
    xmlns:tet="http://www.onvif.org/ver10/events/wsdl"
    xmlns:tns1="http://www.onvif.org/ver10/topics"
    xmlns:tt="http://www.onvif.org/ver10/schema">
    <SOAP-ENV:Header>
      <wsa:Action>
        http://www.onvif.org/ver10/events/wsdl/PullPointSubscription/PullMessagesResponse
      </wsa:Action>
    </SOAP-ENV:Header>
    <SOAP-ENV:Body>
      <tet:PullMessagesResponse>
        <tet:CurrentTime>
          2008-10-10T12:24:58
        </tet:CurrentTime>
        <tet:TerminationTime>
          2008-10-10T12:25:58
        </tet:TerminationTime>
        <wsnt:NotificationMessage>
          <wsnt:Topic
            Dialect="http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet">
            tns1:RuleEngine/LineDetector/Crossed
          </wsnt:Topic>
          <wsnt:Message>
            <tt:Message  UtcTime="2008-10-10T12:24:57.321Z">
              <tt:Source>
                <tt:SimpleItem Name="VideoSourceConfigurationToken" Value="1"/>
                <tt:SimpleItem Name="VideoAnalyticsConfigurationToken" Value="2"/>
                <tt:SimpleItem Value="MyImportantFence1" Name="Rule"/>
              </tt:Source>
              <tt:Data>
                <tt:SimpleItem Name="ObjectId" Value="15" />
              </tt:Data>
            </tt:Message>
          </wsnt:Message>
        </wsnt:NotificationMessage>
        <wsnt:NotificationMessage>
          <wsnt:Topic
            Dialect="http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet">
            tns1:RuleEngine/LineDetector/Crossed
          </wsnt:Topic>
          <wsnt:Message>
            <tt:Message UtcTime="2008-10-10T12:24:57.789Z">
              <tt:Source>
                <tt:SimpleItem Name="VideoSourceConfigurationToken" Value="1"/>
                <tt:SimpleItem Name="VideoAnalyticsConfigurationToken" Value="2"/>
                <tt:SimpleItem Value="MyImportantFence2" Name="Rule"/>
              </tt:Source>
              <tt:Data>
                <tt:SimpleItem Name="ObjectId" Value="19"/>
              </tt:Data>
            </tt:Message>
          </wsnt:Message>
        </wsnt:NotificationMessage>
      </tet:PullMessagesResponse>
    </SOAP-ENV:Body>
  </SOAP-ENV:Envelope>

如果沒在gsoap\typemap.dat文件末尾加上那兩行,生成的ONVIF代碼跟PullMessagesResponse相關的結構體定義如下(在soapStub.h頭文件中)。

struct _tev__PullMessagesResponse {
    time_t CurrentTime;
    time_t TerminationTime;
    int __sizeNotificationMessage;
    struct wsnt__NotificationMessageHolderType *wsnt__NotificationMessage;
};

struct wsnt__NotificationMessageHolderType {
    struct wsa5__EndpointReferenceType *SubscriptionReference;
    struct wsnt__TopicExpressionType *Topic;
    struct wsa5__EndpointReferenceType *ProducerReference;
    struct _wsnt__NotificationMessageHolderType_Message Message;
};

struct _wsnt__NotificationMessageHolderType_Message {
    char dummy;
};

gSOAP生成的代碼,結構體變量命名,都是有規律的,跟前面的XML是一一對應,比如:

  • <tet:PullMessagesResponse> 對應着結構體struct _tev__PullMessagesResponse
  • <tet:CurrentTime>對應着結構體成員變量time_t CurrentTime
  • <tet:TerminationTime>對應着結構體成員變量time_t TerminationTime
  • <wsnt:NotificationMessage>對應着結構體成員變量struct wsnt__NotificationMessageHolderType *wsnt__NotificationMessage;
  • <wsnt:Topic對應着struct wsnt__NotificationMessageHolderType結構體中的成員變量struct wsnt__TopicExpressionType *Topic;
  • 以此類推,<wsnt:Message>對應着結構體struct _wsnt__NotificationMessageHolderType_Message Message;
  • 可是到了<tt:Message,結構體定義就沒有變量跟它對應了。最終導致的結果就是,gSOAP生成的代碼接口soap_call___tev__PullMessages無法解析這部分XML數據,即客戶端得不到IPC攝像頭送過來的事件通知消息,從而導致客戶端檢測不到訂閱的事件消息。

在gsoap\typemap.dat文件末尾增加:

_wsnt__NotificationMessageHolderType_Message = $ struct _tt__Message* tt__Message;

對PullMessages相關結構體帶來的變化(對比soapStub.h差異),如下圖所示:



圖6 改typemap.dat給soapStub.h帶來的差別

其實結構體定義的變化,在使用wsdl2h工具生成的頭文件onvif.h也能看出端倪,如下圖所示:



圖7 改typemap.dat給onvif.h帶來的差別

不僅結構體定義發生變化,內部函數實現也有不同,soapC.c源文件中的soap_in__wsnt__NotificationMessageHolderType_Message函數就有差異:如下圖所示:



圖8 改typemap.dat給soapC.c帶來的差別

如此改完,PullMessages才能正確解析出PullMessagesResponse的應答XML消息,客戶端才能探測到事件通知消息。

8.2 CreatePullPointSubscription無法訂閱感興趣的主題

在gsoap\typemap.dat文件末尾增加:

wsnt__FilterType = $ struct wsnt__TopicExpressionType* TopicExpression;

對CreatePullPointSubscription相關結構體帶來的變化(對比soapStub.h差異),如下圖所示:



圖9 改typemap.dat給soapStub.h帶來的差別

修改之前,因爲缺少TopicExpression結構體變量,導致CreatePullPointSubscription接口無法訂閱感興趣的主題。這會帶來一個問題,比如說IPC攝像頭支持十種報警事件,但我只關心其中的遮擋報警,因爲不能過濾主題(即關心所有事件),其他我不關心的事件發生變化也會源源不斷的彙報過來,浪費帶寬資源。

修改之後,有了TopicExpression結構體變量,CreatePullPointSubscription接口就能夠訂閱感興趣的主題了。我們訂閱的事件變化纔會通知,不感興趣的事件發生變化不會通知。

8.3 GetEventProperties無法解析TopicSet字段

在我的demo中,將GetEventProperties應答信息的各個字段打印出來(日誌如下),發現無法解析IPC攝像頭送過來的TopicSet字段,但從ONVIF Device Test Tool工具觀察GetEventProperties應答信息,TopicSet字段信息是很多的。有關於該信息,官網ONVIF Core Specification規格手冊中的GetEventPropertiesResponse章節也有例子說明,但這些信息還不足以解決問題。

================= + dump_tev__GetEventPropertiesResponse + >>>
__sizeTopicNamespaceLocation: 1
TopicNamespaceLocation: (0x917f120)
   |- http://www.onvif.org/onvif/ver10/topics/topicns.xml
wsnt__FixedTopicSet: true
wstop__TopicSet: (0x917ef50)
   |- documentation: (null)
__sizeTopicExpressionDialect: 2
wsnt__TopicExpressionDialect: (0x917eef0)
   |- http://docs.oasis-open.org/wsn/t-1/TopicExpression/Concrete
   |- http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet
__sizeMessageContentFilterDialect: 1
MessageContentFilterDialect: (0x917ef68)
   |- http://www.onvif.org/ver10/tev/messageContentFilter/ItemFilter
__sizeProducerPropertiesFilterDialect: 0
ProducerPropertiesFilterDialect: (null)
__sizeMessageContentSchemaLocation: 1
MessageContentSchemaLocation: (0x917f040)
   |- http://www.onvif.org/onvif/ver10/schema/onvif.xsd
================= - dump_tev__GetEventPropertiesResponse - <<<



圖10 ONVIF Device Test Tool工具中執行GetEventProperties

觀察代碼struct _tev__GetEventPropertiesResponse結構體中的struct wstop__TopicSetType定義,發現只有一個struct wstop__Documentation *documentation;成員變量,如下所示,自然無法解析出上圖的TopicSet字段信息。

struct _tev__GetEventPropertiesResponse {
        int __sizeTopicNamespaceLocation;
        char **TopicNamespaceLocation;
        enum xsd__boolean wsnt__FixedTopicSet;
        struct wstop__TopicSetType *wstop__TopicSet;
        int __sizeTopicExpressionDialect;
        char **wsnt__TopicExpressionDialect;
        int __sizeMessageContentFilterDialect;
        char **MessageContentFilterDialect;
        int __sizeProducerPropertiesFilterDialect;
        char **ProducerPropertiesFilterDialect;
        int __sizeMessageContentSchemaLocation;
        char **MessageContentSchemaLocation;
};

struct wstop__TopicSetType {
        struct wstop__Documentation *documentation;
};

通過以下方法可以獲取到TopicSet字段信息,在gsoap\typemap.dat文件末尾增加:

wstop__TopicSetType = $ _XML __mixed;

對GetEventProperties相關結構體帶來的變化(對比soapStub.h差異),如下圖所示:



圖11 改typemap.dat給soapStub.h帶來的差別

即struct wstop__TopicSetType結構體多一個char *__mixed變量,通過這個變量,就能獲取TopicSet字段信息,如下所示。當然這種方式獲取的是XML字符串,不利於應用層解析,是否有更好的方法,還有待研究。

================= + dump_tev__GetEventPropertiesResponse + >>>
__sizeTopicNamespaceLocation: 1
TopicNamespaceLocation: (0x8cee020)
   |- http://www.onvif.org/onvif/ver10/topics/topicns.xml
wsnt__FixedTopicSet: true
wstop__TopicSet: (0x8cedf50)
   |- documentation: (null)
   |- __mixed: <tns1:RuleEngine><CellMotionDetector><Motion wstop:topic="true"><tt:MessageDescription IsProperty="true"><tt:Source><tt:SimpleItemDescription Name="VideoSourceConfigurationToken" Type="tt:ReferenceToken"/><tt:SimpleItemDescription Name="VideoAnalyticsConfigurationToken" Type="tt:ReferenceToken"/><tt:SimpleItemDescription Name="Rule" Type="xs:string"/></tt:Source><tt:Data><tt:SimpleItemDescription Name="IsMotion" Type="xs:boolean"/></tt:Data></tt:MessageDescription></Motion></CellMotionDetector><TamperDetector><Tamper wstop:topic="true"><tt:MessageDescription IsProperty="true"><tt:Source><tt:SimpleItemDescription Name="VideoSourceConfigurationToken" Type="tt:ReferenceToken"/><tt:SimpleItemDescription Name="TamperWindowIndex" Type="xs:string"/></tt:Source><tt:Data><tt:SimpleItemDescription Name="IsTamper" Type="xs:string"/></tt:Data></tt:MessageDescription></Tamper></TamperDetector></tns1:RuleEngine>
__sizeTopicExpressionDialect: 2
wsnt__TopicExpressionDialect: (0x8cedef0)
   |- http://docs.oasis-open.org/wsn/t-1/TopicExpression/Concrete
   |- http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet
__sizeMessageContentFilterDialect: 1
MessageContentFilterDialect: (0x8ced140)
   |- http://www.onvif.org/ver10/tev/messageContentFilter/ItemFilter
__sizeProducerPropertiesFilterDialect: 0
ProducerPropertiesFilterDialect: (null)
__sizeMessageContentSchemaLocation: 1
MessageContentSchemaLocation: (0x8ced218)
   |- http://www.onvif.org/onvif/ver10/schema/onvif.xsd
================= - dump_tev__GetEventPropertiesResponse - <<<

9 其他問題

9.1 各類事件的主題Topic是什麼

使用CreatePullPointSubscription訂閱主題時,需要指定topic,並設定過濾規則,以下是常用事件的topic:

  • 遮擋報警:tns1:RuleEngine/TamperDetector/Tamper
  • 移動偵測:tns1:RuleEngine/CellMotionDetector/Motion

ONVIF還支持其他事件,它們的topic又是什麼呢,在「ONVIF-Core-Specification-v250」版本中的「ONVIF Topic Namespace」章節有提到,如下圖所示,但也僅僅是提到root topics,信息也不足,這方面的知識還有待研究。奇怪的是,ONVIF官方打從「ONVIF-Core-Specification-v260」版本就被刪掉了這部分信息,也不知道是幾個意思。



圖12 ONVIF Topic Namespace

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章