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

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

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