From ec9403fb6f6635cf42d0cf7fd82bb40894f8769e Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Tue, 25 Jan 2022 22:27:11 -0600 Subject: [PATCH] Adds Amcrest On-camera Motion Detection --- CMakeLists.txt | 2 +- db/zm_create.sql.in | 1 + db/zm_update-1.37.11.sql | 18 ++ scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm | 1 + src/zm_monitor.cpp | 219 ++++++++++++++----- src/zm_monitor.h | 11 +- web/includes/Monitor.php | 1 + web/skins/classic/views/js/monitor.js | 17 ++ web/skins/classic/views/monitor.php | 4 + 9 files changed, 214 insertions(+), 60 deletions(-) create mode 100644 db/zm_update-1.37.11.sql diff --git a/CMakeLists.txt b/CMakeLists.txt index 7df6aaa70..1a65f85eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -318,7 +318,7 @@ endif() # Do not check for cURL if ZM_NO_CURL is on if(NOT ZM_NO_CURL) # cURL - find_package(CURL) + find_package(CURL REQUIRED) if(CURL_FOUND) set(HAVE_LIBCURL 1) list(APPEND ZM_BIN_LIBS ${CURL_LIBRARIES}) diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index 7dc77cc47..040bc593c 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -467,6 +467,7 @@ CREATE TABLE `Monitors` ( `ONVIF_Password` VARCHAR(64) NOT NULL DEFAULT '', `ONVIF_Options` VARCHAR(64) NOT NULL DEFAULT '', `ONVIF_Event_Listener` BOOLEAN NOT NULL DEFAULT FALSE, + `use_Amcrest_API` BOOLEAN NOT NULL DEFAULT FALSE, `Device` tinytext NOT NULL default '', `Channel` tinyint(3) unsigned NOT NULL default '0', `Format` int(10) unsigned NOT NULL default '0', diff --git a/db/zm_update-1.37.11.sql b/db/zm_update-1.37.11.sql new file mode 100644 index 000000000..58a3d0cf1 --- /dev/null +++ b/db/zm_update-1.37.11.sql @@ -0,0 +1,18 @@ +-- +-- Update Monitors table to have use_Amcrest_API +-- + +SELECT 'Checking for use_Amcrest_API in Monitors'; +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Monitors' + AND table_schema = DATABASE() + AND column_name = 'use_Amcrest_API' + ) > 0, +"SELECT 'Column use_Amcrest_API already exists in Monitors'", +"ALTER TABLE `Monitors` ADD COLUMN `use_Amcrest_API` BOOLEAN NOT NULL default false AFTER `ONVIF_Event_Listener`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm b/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm index 54ce25c87..10cfffe8b 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm @@ -63,6 +63,7 @@ $serial = $primary_key = 'Id'; ONVIF_Password ONVIF_Options ONVIF_Event_Listener + use_Amcrest_API Device Channel Format diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 4ae155e23..558a30380 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -92,7 +92,7 @@ std::string load_monitor_sql = "`SectionLength`, `MinSectionLength`, `FrameSkip`, `MotionFrameSkip`, " "`FPSReportInterval`, `RefBlendPerc`, `AlarmRefBlendPerc`, `TrackMotion`, `Exif`," "`RTSPServer`, `RTSPStreamName`," -"`ONVIF_URL`, `ONVIF_Username`, `ONVIF_Password`, `ONVIF_Options`, `ONVIF_Event_Listener`, " +"`ONVIF_URL`, `ONVIF_Username`, `ONVIF_Password`, `ONVIF_Options`, `ONVIF_Event_Listener`, `use_Amcrest_API`, " "`SignalCheckPoints`, `SignalCheckColour`, `Importance`-1 FROM `Monitors`"; std::string CameraType_Strings[] = { @@ -461,7 +461,7 @@ Monitor::Monitor() "SectionLength, MinSectionLength, FrameSkip, MotionFrameSkip, " "FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, Exif," "`RTSPServer`,`RTSPStreamName`, - "`ONVIF_URL`, `ONVIF_Username`, `ONVIF_Password`, `ONVIF_Options`, `ONVIF_Event_Listener`, " + "`ONVIF_URL`, `ONVIF_Username`, `ONVIF_Password`, `ONVIF_Options`, `ONVIF_Event_Listener`, `use_Amcrest_API`, " "SignalCheckPoints, SignalCheckColour, Importance-1 FROM Monitors"; */ @@ -639,6 +639,7 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) { onvif_password = std::string(dbrow[col] ? dbrow[col] : ""); col++; onvif_options = std::string(dbrow[col] ? dbrow[col] : ""); col++; onvif_event_listener = (*dbrow[col] != '0'); col++; + use_Amcrest_API = (*dbrow[col] != '0'); col++; /*"SignalCheckPoints, SignalCheckColour, Importance-1 FROM Monitors"; */ signal_check_points = atoi(dbrow[col]); col++; @@ -1040,6 +1041,7 @@ bool Monitor::connect() { Debug(3, "Allocated %zu %zu image buffers", image_buffer.capacity(), image_buffer.size()); if (purpose == CAPTURE) { + curl_global_init(CURL_GLOBAL_DEFAULT); //May not be the appropriate place. Need to do this before any other curl calls, and any other threads start. memset(mem_ptr, 0, mem_size); shared_data->size = sizeof(SharedData); shared_data->active = enabled; @@ -1079,47 +1081,59 @@ bool Monitor::connect() { usedsubpixorder = camera->SubpixelOrder(); // Used in CheckSignal shared_data->valid = true; -#ifdef WITH_GSOAP - //ONVIF Setup + //ONVIF and Amcrest Setup + //since they serve the same function, handling them as two options of the same feature. ONVIF_Trigger_State = FALSE; - if (onvif_event_listener) { //Temporarily using this option to enable the feature + if (onvif_event_listener) { // Debug(1, "Starting ONVIF"); ONVIF_Healthy = FALSE; if (onvif_options.find("closes_event") != std::string::npos) { //Option to indicate that ONVIF will send a close event message ONVIF_Closes_Event = TRUE; } - tev__PullMessages.Timeout = "PT600S"; - tev__PullMessages.MessageLimit = 100; - soap = soap_new(); - soap->connect_timeout = 5; - soap->recv_timeout = 5; - soap->send_timeout = 5; - soap_register_plugin(soap, soap_wsse); - proxyEvent = PullPointSubscriptionBindingProxy(soap); - std::string full_url = onvif_url + "/Events"; - proxyEvent.soap_endpoint = full_url.c_str(); + if (use_Amcrest_API) { + curl_multi = curl_multi_init(); + start_Amcrest(); + //spin up curl_multi + //use the onvif_user and onvif_pass and onvif_url here. + //going to use the non-blocking curl api, and in the polling thread, block for 5 seconds waiting for input, just like onvif + //note that it's not possible for a single camera to use both. + } else { //using GSOAP +#ifdef WITH_GSOAP + tev__PullMessages.Timeout = "PT600S"; + tev__PullMessages.MessageLimit = 100; + soap = soap_new(); + soap->connect_timeout = 5; + soap->recv_timeout = 5; + soap->send_timeout = 5; + soap_register_plugin(soap, soap_wsse); + proxyEvent = PullPointSubscriptionBindingProxy(soap); + std::string full_url = onvif_url + "/Events"; + proxyEvent.soap_endpoint = full_url.c_str(); - set_credentials(soap); - Debug(1, "ONVIF Endpoint: %s", proxyEvent.soap_endpoint); - if (proxyEvent.CreatePullPointSubscription(&request, response) != SOAP_OK) { - Error("Couldn't create subscription! %s, %s", soap_fault_string(soap), soap_fault_detail(soap)); - } else { - //Empty the stored messages set_credentials(soap); - if ((proxyEvent.PullMessages(response.SubscriptionReference.Address, NULL, &tev__PullMessages, tev__PullMessagesResponse) != SOAP_OK) && - ( soap->error != SOAP_EOF)) { //SOAP_EOF could indicate no messages to pull. - Error("Couldn't do initial event pull! Error %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap)); + Debug(1, "ONVIF Endpoint: %s", proxyEvent.soap_endpoint); + if (proxyEvent.CreatePullPointSubscription(&request, response) != SOAP_OK) { + Error("Couldn't create subscription! %s, %s", soap_fault_string(soap), soap_fault_detail(soap)); } else { - Debug(1, "Good Initial ONVIF Pull"); - ONVIF_Healthy = TRUE; + //Empty the stored messages + set_credentials(soap); + if ((proxyEvent.PullMessages(response.SubscriptionReference.Address, NULL, &tev__PullMessages, tev__PullMessagesResponse) != SOAP_OK) && + ( soap->error != SOAP_EOF)) { //SOAP_EOF could indicate no messages to pull. + Error("Couldn't do initial event pull! Error %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap)); + } else { + Debug(1, "Good Initial ONVIF Pull"); + ONVIF_Healthy = TRUE; + } } +#else + Error("zmc not compiled with GSOAP. ONVIF support not built in!"); +#endif } } else { Debug(1, "Not Starting ONVIF"); } //End ONVIF Setup -#endif #if HAVE_LIBCURL //janus setup. Depends on libcurl. if (janus_enabled && (path.find("rtsp://") != std::string::npos)) { @@ -1228,6 +1242,12 @@ Monitor::~Monitor() { sws_freeContext(convert_context); convert_context = nullptr; } + if (Amcrest_handle != nullptr) { + curl_multi_remove_handle(curl_multi, Amcrest_handle); + curl_easy_cleanup(Amcrest_handle); + } + if (curl_multi != nullptr) curl_multi_cleanup(curl_multi); + curl_global_cleanup(); } // end Monitor::~Monitor() void Monitor::AddPrivacyBitmask() { @@ -1800,36 +1820,25 @@ bool Monitor::Poll() { //We want to trigger every 5 seconds or so. so grab the time at the beginning of the loop, and sleep at the end. std::chrono::system_clock::time_point loop_start_time = std::chrono::system_clock::now(); -#ifdef WITH_GSOAP if (ONVIF_Healthy) { - set_credentials(soap); - int result = proxyEvent.PullMessages(response.SubscriptionReference.Address, NULL, &tev__PullMessages, tev__PullMessagesResponse); - if (result != SOAP_OK) { - if (result != SOAP_EOF) { //Ignore the timeout error - Error("Failed to get ONVIF messages! %s", soap_fault_string(soap)); - ONVIF_Healthy = FALSE; - } - } else { - Debug(1, "Got Good Response! %i", result); - for (auto msg : tev__PullMessagesResponse.wsnt__NotificationMessage) { - if (msg->Topic->__any.text != NULL && - std::strstr(msg->Topic->__any.text, "MotionAlarm") && - msg->Message.__any.elts != NULL && - msg->Message.__any.elts->next != NULL && - msg->Message.__any.elts->next->elts != NULL && - msg->Message.__any.elts->next->elts->atts != NULL && - msg->Message.__any.elts->next->elts->atts->next != NULL && - msg->Message.__any.elts->next->elts->atts->next->text != NULL) { - Debug(1,"Got Motion Alarm!"); - if (strcmp(msg->Message.__any.elts->next->elts->atts->next->text, "true") == 0) { + if(use_Amcrest_API) { + int open_handles; + int transfers; + curl_multi_perform(curl_multi, &open_handles); + if (open_handles == 0) { + start_Amcrest(); //http transfer ended, need to restart. + } else { + curl_multi_wait(curl_multi, NULL, 0, 5000, &transfers); //wait for max 5 seconds for event. + if (transfers > 0) { //have data to deal with + curl_multi_perform(curl_multi, &open_handles); //actually grabs the data, populates amcrest_response + if (amcrest_response.find("action=Start") != std::string::npos) { //Event Start - Debug(1,"Triggered on ONVIF"); + Debug(1,"Triggered on ONVIF"); if (!ONVIF_Trigger_State) { Debug(1,"Triggered Event"); ONVIF_Trigger_State = TRUE; - std::this_thread::sleep_for (std::chrono::seconds(1)); //thread sleep } - } else { + } else if (amcrest_response.find("action=Stop") != std::string::npos){ Debug(1, "Triggered off ONVIF"); ONVIF_Trigger_State = FALSE; if (!ONVIF_Closes_Event) { //If we get a close event, then we know to expect them. @@ -1837,11 +1846,53 @@ bool Monitor::Poll() { Debug(1,"Setting ClosesEvent"); } } + amcrest_response.clear(); //We've dealt with the message, need to clear the queue } } + } else { + +#ifdef WITH_GSOAP + set_credentials(soap); + int result = proxyEvent.PullMessages(response.SubscriptionReference.Address, NULL, &tev__PullMessages, tev__PullMessagesResponse); + if (result != SOAP_OK) { + if (result != SOAP_EOF) { //Ignore the timeout error + Error("Failed to get ONVIF messages! %s", soap_fault_string(soap)); + ONVIF_Healthy = FALSE; + } + } else { + Debug(1, "Got Good Response! %i", result); + for (auto msg : tev__PullMessagesResponse.wsnt__NotificationMessage) { + if (msg->Topic->__any.text != NULL && + std::strstr(msg->Topic->__any.text, "MotionAlarm") && + msg->Message.__any.elts != NULL && + msg->Message.__any.elts->next != NULL && + msg->Message.__any.elts->next->elts != NULL && + msg->Message.__any.elts->next->elts->atts != NULL && + msg->Message.__any.elts->next->elts->atts->next != NULL && + msg->Message.__any.elts->next->elts->atts->next->text != NULL) { + Debug(1,"Got Motion Alarm!"); + if (strcmp(msg->Message.__any.elts->next->elts->atts->next->text, "true") == 0) { + //Event Start + Debug(1,"Triggered on ONVIF"); + if (!ONVIF_Trigger_State) { + Debug(1,"Triggered Event"); + ONVIF_Trigger_State = TRUE; + std::this_thread::sleep_for (std::chrono::seconds(1)); //thread sleep + } + } else { + Debug(1, "Triggered off ONVIF"); + ONVIF_Trigger_State = FALSE; + if (!ONVIF_Closes_Event) { //If we get a close event, then we know to expect them. + ONVIF_Closes_Event = TRUE; + Debug(1,"Setting ClosesEvent"); + } + } + } + } + } +#endif } } -#endif if (janus_enabled) { if (janus_session.empty()) { get_janus_session(); @@ -3223,11 +3274,15 @@ int Monitor::Close() { analysis_thread->Stop(); } -#ifdef WITH_GSOAP //ONVIF Teardown if (Poller) { Poller->Stop(); } + if (curl_multi != nullptr) { + curl_multi_cleanup(curl_multi); + curl_multi = nullptr; + } +#ifdef WITH_GSOAP if (onvif_event_listener && (soap != nullptr)) { Debug(1, "Tearing Down Onvif"); _wsnt__Unsubscribe wsnt__Unsubscribe; @@ -3240,9 +3295,9 @@ int Monitor::Close() { } //End ONVIF #endif #if HAVE_LIBCURL //Janus Teardown - if (janus_enabled && (purpose == CAPTURE)) { - remove_from_janus(); - } + if (janus_enabled && (purpose == CAPTURE)) { + remove_from_janus(); + } #endif packetqueue.clear(); @@ -3614,3 +3669,55 @@ int Monitor::get_janus_session() { curl_easy_cleanup(curl); return 1; } //get_janus_session + +int Monitor::start_Amcrest() { + //init the transfer and start it in multi-handle + int running_handles; + long response_code; + struct CURLMsg *m; + CURLMcode curl_error; + if (Amcrest_handle != nullptr) { //potentially clean up the old handle + curl_multi_remove_handle(curl_multi, Amcrest_handle); + curl_easy_cleanup(Amcrest_handle); + } + + std::string full_url = onvif_url; + if (full_url.back() != '/') full_url += '/'; + full_url += "eventManager.cgi?action=attach&codes=[VideoMotion]"; + Amcrest_handle = curl_easy_init(); + if (!Amcrest_handle){ + Warning("Handle is null!"); + return -1; + } + curl_easy_setopt(Amcrest_handle, CURLOPT_URL, full_url.c_str()); + curl_easy_setopt(Amcrest_handle, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(Amcrest_handle, CURLOPT_WRITEDATA, &amcrest_response); + curl_easy_setopt(Amcrest_handle, CURLOPT_USERNAME, onvif_username.c_str()); + curl_easy_setopt(Amcrest_handle, CURLOPT_PASSWORD, onvif_password.c_str()); + curl_easy_setopt(Amcrest_handle, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); + curl_error = curl_multi_add_handle(curl_multi, Amcrest_handle); + Warning("error of %i", curl_error); + curl_error = curl_multi_perform(curl_multi, &running_handles); + if (curl_error == CURLM_OK) { + curl_easy_getinfo(Amcrest_handle, CURLINFO_RESPONSE_CODE, &response_code); + int msgq = 0; + m = curl_multi_info_read(curl_multi, &msgq); + if (m && (m->msg == CURLMSG_DONE)) { + Warning("Libcurl exited Early: %i", m->data.result); + } + + curl_multi_wait(curl_multi, NULL, 0, 300, NULL); + curl_error = curl_multi_perform(curl_multi, &running_handles); + } + + if ((curl_error == CURLM_OK) && (running_handles > 0)) { + ONVIF_Healthy = TRUE; + } else { + Warning("Response: %s", amcrest_response.c_str()); + Warning("Seeing %i streams, and error of %i, url: %s", running_handles, curl_error, full_url.c_str()); + curl_easy_getinfo(Amcrest_handle, CURLINFO_OS_ERRNO, &response_code); + Warning("Response code: %lu", response_code); + } + +return 0; +} diff --git a/src/zm_monitor.h b/src/zm_monitor.h index 98c36e5a9..7a4b7ad83 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -285,7 +285,9 @@ protected: std::string onvif_username; std::string onvif_password; std::string onvif_options; + std::string amcrest_response; bool onvif_event_listener; + bool use_Amcrest_API; std::string device; int palette; @@ -437,7 +439,7 @@ protected: //ONVIF #ifdef WITH_GSOAP - struct soap *soap; + struct soap *soap = nullptr; bool ONVIF_Trigger_State; bool ONVIF_Healthy; bool ONVIF_Closes_Event; @@ -449,8 +451,11 @@ protected: void set_credentials(struct soap *soap); #endif - //curl stuff for Janus - CURL *curl; + //curl stuff + CURL *curl = nullptr; + CURLM *curl_multi = nullptr; + CURL *Amcrest_handle = nullptr; + int start_Amcrest(); //helper class for CURL static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp); int add_to_janus(); diff --git a/web/includes/Monitor.php b/web/includes/Monitor.php index 09d4657a8..9c7d8d25c 100644 --- a/web/includes/Monitor.php +++ b/web/includes/Monitor.php @@ -66,6 +66,7 @@ class Monitor extends ZM_Object { 'ONVIF_Password' => '', 'ONVIF_Options' => '', 'ONVIF_Event_Listener' => '0', + 'use_Amcrest_API' => '0', 'Device' => '', 'Channel' => 0, 'Format' => '0', diff --git a/web/skins/classic/views/js/monitor.js b/web/skins/classic/views/js/monitor.js index 62c0c6514..30a906df6 100644 --- a/web/skins/classic/views/js/monitor.js +++ b/web/skins/classic/views/js/monitor.js @@ -275,6 +275,23 @@ function initPage() { } }); + // Amcrest API controller + if (document.getElementsByName("newMonitor[ONVIF_Event_Listener]")[0].checked) { + document.getElementById("function_use_Amcrest_API").hidden = false; + } else { + document.getElementById("function_use_Amcrest_API").hidden = true; + } + document.getElementsByName("newMonitor[ONVIF_Event_Listener]")[0].addEventListener('change', function() { + if (this.checked) { + document.getElementById("function_use_Amcrest_API").hidden = false; + } + }); + document.getElementsByName("newMonitor[ONVIF_Event_Listener]")[1].addEventListener('change', function() { + if (this.checked) { + document.getElementById("function_use_Amcrest_API").hidden = true; + } + }); + if ( ZM_OPT_USE_GEOLOCATION ) { if ( window.L ) { var form = document.getElementById('contentForm'); diff --git a/web/skins/classic/views/monitor.php b/web/skins/classic/views/monitor.php index 6756c239b..deba63879 100644 --- a/web/skins/classic/views/monitor.php +++ b/web/skins/classic/views/monitor.php @@ -730,6 +730,10 @@ if (count($available_monitor_ids)) { translate('Enabled'), '0'=>'Disabled'), $monitor->ONVIF_Event_Listener()); ?> + + + translate('Enabled'), '0'=>'Disabled'), $monitor->use_Amcrest_API()); ?> +