From 89e1c5d53c31776d2c5e0e2f29468d3c0896bd8a Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 26 Jan 2022 16:54:27 -0600 Subject: [PATCH 01/21] Move Monitor::MonitorLink to dedicated file --- src/CMakeLists.txt | 1 + src/zm_monitor_monitorlink.cpp | 198 +++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 src/zm_monitor_monitorlink.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6878a6589..439a77291 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -33,6 +33,7 @@ set(ZM_BIN_SRC_FILES zm_libvnc_camera.cpp zm_local_camera.cpp zm_monitor.cpp + zm_monitor_monitorlink.cpp zm_monitorstream.cpp zm_ffmpeg.cpp zm_ffmpeg_camera.cpp diff --git a/src/zm_monitor_monitorlink.cpp b/src/zm_monitor_monitorlink.cpp new file mode 100644 index 000000000..95432388a --- /dev/null +++ b/src/zm_monitor_monitorlink.cpp @@ -0,0 +1,198 @@ +// +// ZoneMinder Monitor Class Implementation, $Date$, $Revision$ +// Copyright (C) 2001-2008 Philip Coombes +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// + +#include "zm_monitor.h" + +#include + +#if ZM_MEM_MAPPED +#include +#include +#else // ZM_MEM_MAPPED +#include +#include +#endif // ZM_MEM_MAPPED + +Monitor::MonitorLink::MonitorLink(unsigned int p_id, const char *p_name) : + id(p_id), + shared_data(nullptr), + trigger_data(nullptr), + video_store_data(nullptr) +{ + strncpy(name, p_name, sizeof(name)-1); + +#if ZM_MEM_MAPPED + map_fd = -1; + mem_file = stringtf("%s/zm.mmap.%u", staticConfig.PATH_MAP.c_str(), id); +#else // ZM_MEM_MAPPED + shm_id = 0; +#endif // ZM_MEM_MAPPED + mem_size = 0; + mem_ptr = nullptr; + + last_event_id = 0; + last_state = IDLE; + + last_connect_time = 0; + connected = false; +} + +Monitor::MonitorLink::~MonitorLink() { + disconnect(); +} + +bool Monitor::MonitorLink::connect() { + SystemTimePoint now = std::chrono::system_clock::now(); + if (!last_connect_time || (now - std::chrono::system_clock::from_time_t(last_connect_time)) > Seconds(60)) { + last_connect_time = std::chrono::system_clock::to_time_t(now); + + mem_size = sizeof(SharedData) + sizeof(TriggerData); + + Debug(1, "link.mem.size=%jd", static_cast(mem_size)); +#if ZM_MEM_MAPPED + map_fd = open(mem_file.c_str(), O_RDWR, (mode_t)0600); + if (map_fd < 0) { + Debug(3, "Can't open linked memory map file %s: %s", mem_file.c_str(), strerror(errno)); + disconnect(); + return false; + } + while (map_fd <= 2) { + int new_map_fd = dup(map_fd); + Warning("Got one of the stdio fds for our mmap handle. map_fd was %d, new one is %d", map_fd, new_map_fd); + close(map_fd); + map_fd = new_map_fd; + } + + struct stat map_stat; + if (fstat(map_fd, &map_stat) < 0) { + Error("Can't stat linked memory map file %s: %s", mem_file.c_str(), strerror(errno)); + disconnect(); + return false; + } + + if (map_stat.st_size == 0) { + Error("Linked memory map file %s is empty: %s", mem_file.c_str(), strerror(errno)); + disconnect(); + return false; + } else if (map_stat.st_size < mem_size) { + Error("Got unexpected memory map file size %ld, expected %jd", map_stat.st_size, static_cast(mem_size)); + disconnect(); + return false; + } + + mem_ptr = (unsigned char *)mmap(nullptr, mem_size, PROT_READ|PROT_WRITE, MAP_SHARED, map_fd, 0); + if (mem_ptr == MAP_FAILED) { + Error("Can't map file %s (%jd bytes) to memory: %s", mem_file.c_str(), static_cast(mem_size), strerror(errno)); + disconnect(); + return false; + } +#else // ZM_MEM_MAPPED + shm_id = shmget((config.shm_key&0xffff0000)|id, mem_size, 0700); + if (shm_id < 0) { + Debug(3, "Can't shmget link memory: %s", strerror(errno)); + connected = false; + return false; + } + mem_ptr = (unsigned char *)shmat(shm_id, 0, 0); + if ((int)mem_ptr == -1) { + Debug(3, "Can't shmat link memory: %s", strerror(errno)); + connected = false; + return false; + } +#endif // ZM_MEM_MAPPED + + shared_data = (SharedData *)mem_ptr; + trigger_data = (TriggerData *)((char *)shared_data + sizeof(SharedData)); + + if (!shared_data->valid) { + Debug(3, "Linked memory not initialised by capture daemon"); + disconnect(); + return false; + } + + last_state = shared_data->state; + last_event_id = shared_data->last_event_id; + connected = true; + + return true; + } + return false; +} // end bool Monitor::MonitorLink::connect() + +bool Monitor::MonitorLink::disconnect() { + if (connected) { + connected = false; + +#if ZM_MEM_MAPPED + if (mem_ptr > (void *)0) { + msync(mem_ptr, mem_size, MS_ASYNC); + munmap(mem_ptr, mem_size); + } + if (map_fd >= 0) + close(map_fd); + + map_fd = -1; +#else // ZM_MEM_MAPPED + struct shmid_ds shm_data; + if (shmctl(shm_id, IPC_STAT, &shm_data) < 0) { + Debug(3, "Can't shmctl: %s", strerror(errno)); + return false; + } + + shm_id = 0; + + if (shm_data.shm_nattch <= 1) { + if (shmctl(shm_id, IPC_RMID, 0) < 0) { + Debug(3, "Can't shmctl: %s", strerror(errno)); + return false; + } + } + + if (shmdt(mem_ptr) < 0) { + Debug(3, "Can't shmdt: %s", strerror(errno)); + return false; + } +#endif // ZM_MEM_MAPPED + mem_size = 0; + mem_ptr = nullptr; + } + return true; +} + +bool Monitor::MonitorLink::isAlarmed() { + if (!connected) { + return false; + } + return( shared_data->state == ALARM ); +} + +bool Monitor::MonitorLink::inAlarm() { + if (!connected) { + return false; + } + return( shared_data->state == ALARM || shared_data->state == ALERT ); +} + +bool Monitor::MonitorLink::hasAlarmed() { + if (shared_data->state == ALARM) { + return true; + } + last_event_id = shared_data->last_event_id; + return false; +} From 7515711eb848293542a6f5a8f123c9893ae0a90c Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 3 Feb 2022 14:45:17 -0500 Subject: [PATCH 02/21] Implement Server function which figures out which Server likely has the video. Use it to remove duplicate logic --- web/includes/Event.php | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/web/includes/Event.php b/web/includes/Event.php index e042849e9..6f180ef17 100644 --- a/web/includes/Event.php +++ b/web/includes/Event.php @@ -214,22 +214,24 @@ class Event extends ZM_Object { } } # end Event->delete - public function getStreamSrc( $args=array(), $querySep='&' ) { - - $streamSrc = ''; - $Server = null; + public function Server() { if ( $this->Storage()->ServerId() ) { # The Event may have been moved to Storage on another server, # So prefer viewing the Event from the Server that is actually # storing the video - $Server = $this->Storage()->Server(); + return $this->Storage()->Server(); } else if ( $this->Monitor()->ServerId() ) { # Assume that the server that recorded it has it - $Server = $this->Monitor()->Server(); - } else { - # A default Server will result in the use of ZM_DIR_EVENTS - $Server = new Server(); + return $this->Monitor()->Server(); } + # A default Server will result in the use of ZM_DIR_EVENTS + return new Server(); + } + + public function getStreamSrc( $args=array(), $querySep='&' ) { + + $streamSrc = ''; + $Server = $this->Server(); # If we are in a multi-port setup, then use the multiport, else by # passing null Server->Url will use the Port set in the Server setting @@ -354,15 +356,7 @@ class Event extends ZM_Object { # We always store at least 1 image when capturing $streamSrc = ''; - $Server = null; - if ( $this->Storage()->ServerId() ) { - $Server = $this->Storage()->Server(); - } else if ( $this->Monitor()->ServerId() ) { - # Assume that the server that recorded it has it - $Server = $this->Monitor()->Server(); - } else { - $Server = new Server(); - } + $Server = $this->Server(); $streamSrc .= $Server->UrlToIndex( ZM_MIN_STREAMING_PORT ? ZM_MIN_STREAMING_PORT+$this->{'MonitorId'} : @@ -514,7 +508,7 @@ class Event extends ZM_Object { return false; } $Storage= $this->Storage(); - $Server = $Storage->ServerId() ? $Storage->Server() : $this->Monitor()->Server(); + $Server = $this->Server(); if ( $Server->Id() != ZM_SERVER_ID ) { $url = $Server->UrlToApi() . '/events/'.$this->{'Id'}.'.json'; @@ -562,7 +556,7 @@ class Event extends ZM_Object { return false; } $Storage= $this->Storage(); - $Server = $Storage->ServerId() ? $Storage->Server() : $this->Monitor()->Server(); + $Server = $this->Server(); if ( $Server->Id() != ZM_SERVER_ID ) { $url = $Server->UrlToApi().'/events/'.$this->{'Id'}.'.json'; From 7a95aa7210e0484b29b917ddc1792cbce6933eb9 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 3 Feb 2022 14:46:39 -0500 Subject: [PATCH 03/21] Don't render cues if we don't have any. This occurs on initial load we call changeScale which would re-render the cues but the cur ajax hasn't completed yet, so this just avoids an error being logged --- web/skins/classic/views/js/event.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/skins/classic/views/js/event.js b/web/skins/classic/views/js/event.js index ac05f51fe..31363e154 100644 --- a/web/skins/classic/views/js/event.js +++ b/web/skins/classic/views/js/event.js @@ -203,7 +203,10 @@ function changeScale() { streamScale(scale == '0' ? autoScale : scale); drawProgressBar(); } - alarmCue.html(renderAlarmCues(eventViewer));//just re-render alarmCues. skip ajax call + if (cueFrames) { + //just re-render alarmCues. skip ajax call + alarmCue.html(renderAlarmCues(eventViewer)); + } setCookie('zmEventScale'+eventData.MonitorId, scale, 3600); // After a resize, check if we still have room to display the event stats table From 3c655807e841d9194fc9612b9337b6ce85d55796 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 3 Feb 2022 14:47:32 -0500 Subject: [PATCH 04/21] Use new Event->Server function to return the correct (and matching url to zms) url to use for ajax status calls. Fixes errors in a multi-server environment. --- web/skins/classic/views/js/event.js.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/skins/classic/views/js/event.js.php b/web/skins/classic/views/js/event.js.php index 5d4d57f7c..c2b61f395 100644 --- a/web/skins/classic/views/js/event.js.php +++ b/web/skins/classic/views/js/event.js.php @@ -87,7 +87,7 @@ var eventDataStrings = { Emailed: '' }; -var monitorUrl = 'Storage()->Server()->UrlToIndex(); ?>'; +var monitorUrl = 'Server()->UrlToIndex(); ?>'; var filterQuery = ''; var sortQuery = ''; From ac39be33f50403974476c9baa638b71a55291754 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 3 Feb 2022 17:24:33 -0500 Subject: [PATCH 05/21] Don't assume filename of mp4. We store it in the event record for a reason. Fixes #3422 --- scripts/zmfilter.pl.in | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/zmfilter.pl.in b/scripts/zmfilter.pl.in index 960c090cb..3fb286039 100644 --- a/scripts/zmfilter.pl.in +++ b/scripts/zmfilter.pl.in @@ -464,15 +464,18 @@ sub generateImage { my $event_path = $Event->Path(); my $capture_image_path = sprintf('%s/%0'.$Config{ZM_EVENT_IMAGE_DIGITS}.'d-capture.jpg', $event_path, $frame->{FrameId}); my $analyse_image_path = sprintf('%s/%0'.$Config{ZM_EVENT_IMAGE_DIGITS}.'d-analyse.jpg', $event_path, $frame->{FrameId}) if $analyse; - my $video_path = sprintf('%s/%d-video.mp4', $event_path, $Event->{Id}); + my $video_path = sprintf('%s/%s', $event_path, $Event->{DefaultVideo}); my $image_path = ''; # check if the image file exists. If the file doesn't exist and we use H264 try to extract it from .mp4 video if ( $analyse && -r $analyse_image_path ) { + Debug("Using analysis and jpeg exists $analyse_image_path"); $image_path = $analyse_image_path; } elsif ( -r $capture_image_path ) { + Debug("Using captures and jpeg exists $capture_image_path"); $image_path = $capture_image_path; } elsif ( -r $video_path ) { + Debug("mp4 exists $video_path"); my $command ="ffmpeg -nostdin -ss $$frame{Delta} -i '$video_path' -frames:v 1 '$capture_image_path'"; #$command = "ffmpeg -y -v 0 -i $video_path -vf 'select=gte(n\\,$$frame{FrameId}),setpts=PTS-STARTPTS' -vframes 1 -f image2 $capture_image_path"; my $output = qx($command); @@ -486,6 +489,8 @@ sub generateImage { } else { $image_path = $capture_image_path; } + } else { + Debug("No files found at $analyse_image_path, $capture_image_path or $video_path"); } return $image_path; } @@ -723,7 +728,7 @@ sub substituteTags { if ( -e $path ) { push @$attachments_ref, { type=>'image/jpeg', path=>$path }; } else { - Warning("Path to first image does not exist at $path"); + Warning("Path to first image does not exist at $path for image $first_alarm_frame"); } } From a6dc7ba0fc769f5af8aceebaa4669d429df557fe Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 3 Feb 2022 17:30:38 -0500 Subject: [PATCH 06/21] Add debugging, but commented out --- web/includes/Object.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/includes/Object.php b/web/includes/Object.php index ff5d40baa..5023587fd 100644 --- a/web/includes/Object.php +++ b/web/includes/Object.php @@ -237,10 +237,13 @@ class ZM_Object { $changes = array(); if ($defaults) { + // FIXME: This code basically means that the new_values must be a full object, not a subset + // Perhaps if it only concerned itself with the keys of new_values foreach ($defaults as $field => $type) { if (isset($new_values[$field])) continue; if (isset($this->defaults[$field])) { + //Debug("Setting default for $field"); if (is_array($this->defaults[$field])) { $new_values[$field] = $this->defaults[$field]['default']; } else { @@ -255,9 +258,11 @@ class ZM_Object { if (array_key_exists($field, $this->defaults) && is_array($this->defaults[$field]) && isset($this->defaults[$field]['filter_regexp'])) { if (is_array($this->defaults[$field]['filter_regexp'])) { foreach ($this->defaults[$field]['filter_regexp'] as $regexp) { + //Debug("regexping array $field $value to " . preg_replace($regexp, '', trim($value))); $value = preg_replace($regexp, '', trim($value)); } } else { + //Debug("regexping $field $value to " . preg_replace($this->defaults[$field]['filter_regexp'], '', trim($value))); $value = preg_replace($this->defaults[$field]['filter_regexp'], '', trim($value)); } } @@ -265,10 +270,12 @@ class ZM_Object { $old_value = $this->$field(); if (is_array($old_value)) { $diff = array_recursive_diff($old_value, $value); + //Debug("$field array old: " .print_r($old_value, true) . " new: " . print_r($value, true). ' diff: '. print_r($diff, true)); if ( count($diff) ) { $changes[$field] = $value; } } else if ( $this->$field() != $value ) { + //Debug("$field != $value"); $changes[$field] = $value; } } else if (property_exists($this, $field)) { From dde3884084880ba308bad7cab6664ee6ba598f7a Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Thu, 3 Feb 2022 16:31:07 -0600 Subject: [PATCH 07/21] Moves Janus and Amcrest into their own nested classes --- src/CMakeLists.txt | 2 + src/zm_monitor.cpp | 575 +++---------------------------------- src/zm_monitor.h | 79 +++-- src/zm_monitor_amcrest.cpp | 126 ++++++++ src/zm_monitor_janus.cpp | 316 ++++++++++++++++++++ 5 files changed, 538 insertions(+), 560 deletions(-) create mode 100644 src/zm_monitor_amcrest.cpp create mode 100644 src/zm_monitor_janus.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 439a77291..6943a102a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -34,6 +34,8 @@ set(ZM_BIN_SRC_FILES zm_local_camera.cpp zm_monitor.cpp zm_monitor_monitorlink.cpp + zm_monitor_janus.cpp + zm_monitor_amcrest.cpp zm_monitorstream.cpp zm_ffmpeg.cpp zm_ffmpeg_camera.cpp diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index c3333e051..576b4d517 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -130,175 +130,7 @@ std::string TriggerState_Strings[] = { "Cancel", "On", "Off" }; -Monitor::MonitorLink::MonitorLink(unsigned int p_id, const char *p_name) : - id(p_id), - shared_data(nullptr), - trigger_data(nullptr), - video_store_data(nullptr) -{ - strncpy(name, p_name, sizeof(name)-1); - -#if ZM_MEM_MAPPED - map_fd = -1; - mem_file = stringtf("%s/zm.mmap.%u", staticConfig.PATH_MAP.c_str(), id); -#else // ZM_MEM_MAPPED - shm_id = 0; -#endif // ZM_MEM_MAPPED - mem_size = 0; - mem_ptr = nullptr; - - last_event_id = 0; - last_state = IDLE; - - last_connect_time = 0; - connected = false; -} - -Monitor::MonitorLink::~MonitorLink() { - disconnect(); -} - -bool Monitor::MonitorLink::connect() { - SystemTimePoint now = std::chrono::system_clock::now(); - if (!last_connect_time || (now - std::chrono::system_clock::from_time_t(last_connect_time)) > Seconds(60)) { - last_connect_time = std::chrono::system_clock::to_time_t(now); - - mem_size = sizeof(SharedData) + sizeof(TriggerData); - - Debug(1, "link.mem.size=%jd", static_cast(mem_size)); -#if ZM_MEM_MAPPED - map_fd = open(mem_file.c_str(), O_RDWR, (mode_t)0600); - if (map_fd < 0) { - Debug(3, "Can't open linked memory map file %s: %s", mem_file.c_str(), strerror(errno)); - disconnect(); - return false; - } - while (map_fd <= 2) { - int new_map_fd = dup(map_fd); - Warning("Got one of the stdio fds for our mmap handle. map_fd was %d, new one is %d", map_fd, new_map_fd); - close(map_fd); - map_fd = new_map_fd; - } - - struct stat map_stat; - if (fstat(map_fd, &map_stat) < 0) { - Error("Can't stat linked memory map file %s: %s", mem_file.c_str(), strerror(errno)); - disconnect(); - return false; - } - - if (map_stat.st_size == 0) { - Error("Linked memory map file %s is empty: %s", mem_file.c_str(), strerror(errno)); - disconnect(); - return false; - } else if (map_stat.st_size < mem_size) { - Error("Got unexpected memory map file size %ld, expected %jd", map_stat.st_size, static_cast(mem_size)); - disconnect(); - return false; - } - - mem_ptr = (unsigned char *)mmap(nullptr, mem_size, PROT_READ|PROT_WRITE, MAP_SHARED, map_fd, 0); - if (mem_ptr == MAP_FAILED) { - Error("Can't map file %s (%jd bytes) to memory: %s", mem_file.c_str(), static_cast(mem_size), strerror(errno)); - disconnect(); - return false; - } -#else // ZM_MEM_MAPPED - shm_id = shmget((config.shm_key&0xffff0000)|id, mem_size, 0700); - if (shm_id < 0) { - Debug(3, "Can't shmget link memory: %s", strerror(errno)); - connected = false; - return false; - } - mem_ptr = (unsigned char *)shmat(shm_id, 0, 0); - if ((int)mem_ptr == -1) { - Debug(3, "Can't shmat link memory: %s", strerror(errno)); - connected = false; - return false; - } -#endif // ZM_MEM_MAPPED - - shared_data = (SharedData *)mem_ptr; - trigger_data = (TriggerData *)((char *)shared_data + sizeof(SharedData)); - - if (!shared_data->valid) { - Debug(3, "Linked memory not initialised by capture daemon"); - disconnect(); - return false; - } - - last_state = shared_data->state; - last_event_id = shared_data->last_event_id; - connected = true; - - return true; - } - return false; -} // end bool Monitor::MonitorLink::connect() - -bool Monitor::MonitorLink::disconnect() { - if (connected) { - connected = false; - -#if ZM_MEM_MAPPED - if (mem_ptr > (void *)0) { - msync(mem_ptr, mem_size, MS_ASYNC); - munmap(mem_ptr, mem_size); - } - if (map_fd >= 0) - close(map_fd); - - map_fd = -1; -#else // ZM_MEM_MAPPED - struct shmid_ds shm_data; - if (shmctl(shm_id, IPC_STAT, &shm_data) < 0) { - Debug(3, "Can't shmctl: %s", strerror(errno)); - return false; - } - - shm_id = 0; - - if (shm_data.shm_nattch <= 1) { - if (shmctl(shm_id, IPC_RMID, 0) < 0) { - Debug(3, "Can't shmctl: %s", strerror(errno)); - return false; - } - } - - if (shmdt(mem_ptr) < 0) { - Debug(3, "Can't shmdt: %s", strerror(errno)); - return false; - } -#endif // ZM_MEM_MAPPED - mem_size = 0; - mem_ptr = nullptr; - } - return true; -} - -bool Monitor::MonitorLink::isAlarmed() { - if (!connected) { - return false; - } - return( shared_data->state == ALARM ); -} - -bool Monitor::MonitorLink::inAlarm() { - if (!connected) { - return false; - } - return( shared_data->state == ALARM || shared_data->state == ALERT ); -} - -bool Monitor::MonitorLink::hasAlarmed() { - if (shared_data->state == ALARM) { - return true; - } - last_event_id = shared_data->last_event_id; - return false; -} - -Monitor::Monitor() +Monitor::Monitor() : id(0), name(""), server_id(0), @@ -317,7 +149,7 @@ Monitor::Monitor() //user //pass //path - //device + //device palette(0), channel(0), format(0), @@ -422,7 +254,9 @@ Monitor::Monitor() privacy_bitmask(nullptr), n_linked_monitors(0), linked_monitors(nullptr), - ONVIF_Closes_Event(FALSE), + Event_Poller_Closes_Event(FALSE), + Janus_Manager(nullptr), + Amcrest_Manager(nullptr), #ifdef WITH_GSOAP soap(nullptr), #endif @@ -1083,21 +917,16 @@ bool Monitor::connect() { //ONVIF and Amcrest Setup - //since they serve the same function, handling them as two options of the same feature. - ONVIF_Trigger_State = FALSE; + //For now, only support one event type per camera, so share some state. + Poll_Trigger_State = FALSE; if (onvif_event_listener) { // Debug(1, "Starting ONVIF"); - ONVIF_Healthy = FALSE; + Event_Poller_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; + Event_Poller_Closes_Event = TRUE; } 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. + Amcrest_Manager = new AmcrestAPI(this); } else { //using GSOAP #ifdef WITH_GSOAP tev__PullMessages.Timeout = "PT600S"; @@ -1123,7 +952,7 @@ bool Monitor::connect() { 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; + Event_Poller_Healthy = TRUE; } } #else @@ -1135,17 +964,9 @@ bool Monitor::connect() { } //End ONVIF Setup -#if HAVE_LIBCURL //janus setup. Depends on libcurl. - if (janus_enabled && (path.find("rtsp://") != std::string::npos)) { - get_janus_session(); - if (add_to_janus() != 0) { - Warning("Failed to add monitor stream to Janus!"); //The first attempt may fail. Will be reattempted in the Poller thread - } + if (janus_enabled) { + Janus_Manager = new JanusManager(this); } -#else - if (janus_enabled) - Error("zmc not compiled with LIBCURL. Janus support not built in!"); -#endif } else if (!shared_data->valid) { Error("Shared data not initialised by capture daemon for monitor %s", name.c_str()); @@ -1242,12 +1063,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 (Amcrest_Manager != nullptr) { + delete Amcrest_Manager; + } + if (purpose == CAPTURE) { + curl_global_cleanup(); //not sure about this location. } - if (curl_multi != nullptr) curl_multi_cleanup(curl_multi); - curl_global_cleanup(); } // end Monitor::~Monitor() void Monitor::AddPrivacyBitmask() { @@ -1817,38 +1638,13 @@ void Monitor::UpdateFPS() { //Thread where ONVIF polling, and other similar status polling can happen. //Since these can be blocking, run here to avoid intefering with other processing 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(); - if (ONVIF_Healthy) { + if (Event_Poller_Healthy) { 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"); - if (!ONVIF_Trigger_State) { - Debug(1,"Triggered Event"); - ONVIF_Trigger_State = TRUE; - } - } 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. - ONVIF_Closes_Event = TRUE; - Debug(1,"Setting ClosesEvent"); - } - } - amcrest_response.clear(); //We've dealt with the message, need to clear the queue - } - } + Amcrest_Manager->WaitForMessage(); } else { #ifdef WITH_GSOAP @@ -1857,7 +1653,7 @@ bool Monitor::Poll() { 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; + Event_Poller_Healthy = FALSE; } } else { Debug(1, "Got Good Response! %i", result); @@ -1874,16 +1670,16 @@ bool Monitor::Poll() { if (strcmp(msg->Message.__any.elts->next->elts->atts->next->text, "true") == 0) { //Event Start Debug(1,"Triggered on ONVIF"); - if (!ONVIF_Trigger_State) { + if (!Poll_Trigger_State) { Debug(1,"Triggered Event"); - ONVIF_Trigger_State = TRUE; + Poll_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; + Poll_Trigger_State = FALSE; + if (!Event_Poller_Closes_Event) { //If we get a close event, then we know to expect them. + Event_Poller_Closes_Event = TRUE; Debug(1,"Setting ClosesEvent"); } } @@ -1894,11 +1690,9 @@ bool Monitor::Poll() { } } if (janus_enabled) { - if (janus_session.empty()) { - get_janus_session(); - } - if (check_janus() == 0) { - add_to_janus(); + + if (Janus_Manager->check_janus() == 0) { + Janus_Manager->add_to_janus(); } } std::this_thread::sleep_until(loop_start_time + std::chrono::seconds(5)); @@ -1953,8 +1747,8 @@ bool Monitor::Analyse() { Event::StringSetMap noteSetMap; #ifdef WITH_GSOAP - if (onvif_event_listener && ONVIF_Healthy) { - if (ONVIF_Trigger_State) { + if (onvif_event_listener && Event_Poller_Healthy) { + if (Poll_Trigger_State) { score += 9; Debug(1, "Triggered on ONVIF"); Event::StringSet noteSet; @@ -1962,10 +1756,10 @@ bool Monitor::Analyse() { noteSetMap[MOTION_CAUSE] = noteSet; cause += "ONVIF"; //If the camera isn't going to send an event close, we need to close it here, but only after it has actually triggered an alarm. - if (!ONVIF_Closes_Event && state == ALARM) - ONVIF_Trigger_State = FALSE; + if (!Event_Poller_Closes_Event && state == ALARM) + Poll_Trigger_State = FALSE; } // end ONVIF_Trigger - } // end if (onvif_event_listener && ONVIF_Healthy) + } // end if (onvif_event_listener && Event_Poller_Healthy) #endif // Specifically told to be on. Setting the score here is not enough to trigger the alarm. Must jump directly to ALARM @@ -3287,8 +3081,7 @@ int Monitor::PrimeCapture() { } // end if rtsp_server //Poller Thread - if (onvif_event_listener || janus_enabled) { - + if (onvif_event_listener || janus_enabled || use_Amcrest_API) { if (!Poller) { Poller = zm::make_unique(this); } else { @@ -3338,10 +3131,6 @@ int Monitor::Close() { 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"); @@ -3354,11 +3143,11 @@ int Monitor::Close() { soap = nullptr; } //End ONVIF #endif -#if HAVE_LIBCURL //Janus Teardown + //Janus Teardown if (janus_enabled && (purpose == CAPTURE)) { - remove_from_janus(); + delete Janus_Manager; } -#endif + packetqueue.clear(); if (audio_fifo) { @@ -3495,289 +3284,3 @@ int SOAP_ENV__Fault(struct soap *soap, char *faultcode, char *faultstring, char return soap_send_empty_response(soap, SOAP_OK); } #endif - -size_t Monitor::WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) -{ - ((std::string*)userp)->append((char*)contents, size * nmemb); - return size * nmemb; -} - -int Monitor::add_to_janus() { - std::string response; - std::string endpoint; - if ((config.janus_path != nullptr) && (config.janus_path[0] != '\0')) { - endpoint = config.janus_path; - } else { - endpoint = "127.0.0.1:8088/janus/"; - } - std::string postData = "{\"janus\" : \"create\", \"transaction\" : \"randomString\"}"; - std::string rtsp_username; - std::string rtsp_password; - std::string rtsp_path = "rtsp://"; - std::size_t pos; - std::size_t pos2; - CURLcode res; - - curl = curl_easy_init(); - if (!curl) { - Error("Failed to init curl"); - return -1; - } - //parse username and password - pos = path.find(":", 7); - if (pos == std::string::npos) return -1; - rtsp_username = path.substr(7, pos-7); - - pos2 = path.find("@", pos); - if (pos2 == std::string::npos) return -1; - - rtsp_password = path.substr(pos+1, pos2 - pos - 1); - rtsp_path += path.substr(pos2 + 1); - - endpoint += "/"; - endpoint += janus_session; - - //Assemble our actual request - postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; - postData += "\"request\" : \"create\", \"admin_key\" : \""; - postData += config.janus_secret; - postData += "\", \"type\" : \"rtsp\", "; - postData += "\"url\" : \""; - postData += rtsp_path; - postData += "\", \"rtsp_user\" : \""; - postData += rtsp_username; - postData += "\", \"rtsp_pwd\" : \""; - postData += rtsp_password; - postData += "\", \"id\" : "; - postData += std::to_string(id); - if (janus_audio_enabled) postData += ", \"audio\" : true"; - postData += ", \"video\" : true}}"; - - curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); - res = curl_easy_perform(curl); - if (res != CURLE_OK) { - Error("Failed to curl_easy_perform adding rtsp stream"); - curl_easy_cleanup(curl); - return -1; - } - if ((response.find("error") != std::string::npos) && ((response.find("No such session") != std::string::npos) || (response.find("No such handle") != std::string::npos))) { - janus_session = ""; - curl_easy_cleanup(curl); - return -2; - } - //scan for missing session or handle id "No such session" "no such handle" - - Debug(1,"Added stream to Janus: %s", response.c_str()); - curl_easy_cleanup(curl); - return 0; -} - -int Monitor::check_janus() { - std::string response; - std::string endpoint; - if ((config.janus_path != nullptr) && (config.janus_path[0] != '\0')) { - endpoint = config.janus_path; - } else { - endpoint = "127.0.0.1:8088/janus/"; - } - std::string postData; - //std::size_t pos; - CURLcode res; - - curl = curl_easy_init(); - if(!curl) return -1; - - endpoint += "/"; - endpoint += janus_session; - - //Assemble our actual request - postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; - postData += "\"request\" : \"info\", \"id\" : "; - postData += std::to_string(id); - postData += "}}"; - - curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); - res = curl_easy_perform(curl); - if (res != CURLE_OK) { //may mean an error code thrown by Janus, because of a bad session - Warning("Attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res)); - curl_easy_cleanup(curl); - janus_session = ""; - return -1; - } - - curl_easy_cleanup(curl); - Debug(1, "Queried for stream status: %s", response.c_str()); - if ((response.find("error") != std::string::npos) && ((response.find("No such session") != std::string::npos) || (response.find("No such handle") != std::string::npos))) { - Warning("Janus Session timed out"); - janus_session = ""; - return -2; - } else if (response.find("No such mountpoint") != std::string::npos) { - Warning("Mountpoint Missing"); - return 0; - } else { - return 1; - } -} - - -int Monitor::remove_from_janus() { - std::string response; - std::string endpoint; - if ((config.janus_path != nullptr) && (config.janus_path[0] != '\0')) { - endpoint = config.janus_path; - } else { - endpoint = "127.0.0.1:8088/janus/"; - } - std::string postData = "{\"janus\" : \"create\", \"transaction\" : \"randomString\"}"; - //std::size_t pos; - CURLcode res; - - curl = curl_easy_init(); - if(!curl) return -1; - - endpoint += "/"; - endpoint += janus_session; - - //Assemble our actual request - postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; - postData += "\"request\" : \"destroy\", \"admin_key\" : \""; - postData += config.janus_secret; - postData += "\", \"id\" : "; - postData += std::to_string(id); - postData += "}}"; - - curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); - res = curl_easy_perform(curl); - if (res != CURLE_OK) { - Warning("Libcurl attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res)); - curl_easy_cleanup(curl); - return -1; - } - - Debug(1, "Removed stream from Janus: %s", response.c_str()); - curl_easy_cleanup(curl); - return 0; -} - -int Monitor::get_janus_session() { - std::string response; - std::string endpoint; - if ((config.janus_path != nullptr) && (config.janus_path[0] != '\0')) { - endpoint = config.janus_path; - } else { - endpoint = "127.0.0.1:8088/janus/"; - } - std::string postData = "{\"janus\" : \"create\", \"transaction\" : \"randomString\"}"; - std::size_t pos; - CURLcode res; - curl = curl_easy_init(); - if(!curl) return -1; - - //Start Janus API init. Need to get a session_id and handle_id - curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); - res = curl_easy_perform(curl); - if (res != CURLE_OK) { - Warning("Libcurl attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res)); - curl_easy_cleanup(curl); - return -1; - } - - pos = response.find("\"id\": "); - if (pos == std::string::npos) - { - curl_easy_cleanup(curl); - return -1; - } - janus_session = response.substr(pos + 6, 16); - - response = ""; - endpoint += "/"; - endpoint += janus_session; - postData = "{\"janus\" : \"attach\", \"plugin\" : \"janus.plugin.streaming\", \"transaction\" : \"randomString\"}"; - curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); - res = curl_easy_perform(curl); - if (res != CURLE_OK) - { - Warning("Libcurl attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res)); - curl_easy_cleanup(curl); - return -1; - } - - pos = response.find("\"id\": "); - if (pos == std::string::npos) - { - curl_easy_cleanup(curl); - return -1; - } - janus_session += "/"; - janus_session += response.substr(pos + 6, 16); - 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 4ce0a9b08..30bc57d87 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -93,7 +93,7 @@ public: } Orientation; typedef enum { - DEINTERLACE_DISABLED = 0x00000000, + DEINTERLACE_DISABLED = 0x00000000, DEINTERLACE_FOUR_FIELD_SOFT = 0x00001E04, DEINTERLACE_FOUR_FIELD_MEDIUM = 0x00001404, DEINTERLACE_FOUR_FIELD_HARD = 0x00000A04, @@ -154,12 +154,12 @@ protected: uint32_t last_frame_score; /* +60 */ uint32_t audio_frequency; /* +64 */ uint32_t audio_channels; /* +68 */ - /* + /* ** This keeps 32bit time_t and 64bit time_t identical and compatible as long as time is before 2038. ** Shared memory layout should be identical for both 32bit and 64bit and is multiples of 16. - ** Because startup_time is 64bit it may be aligned to a 64bit boundary. So it's offset SHOULD be a multiple + ** Because startup_time is 64bit it may be aligned to a 64bit boundary. So it's offset SHOULD be a multiple ** of 8. Add or delete epadding's to achieve this. - */ + */ union { /* +72 */ time_t startup_time; /* When the zmc process started. zmwatch uses this to see how long the process has been running without getting any images */ uint64_t extrapad1; @@ -256,7 +256,49 @@ protected: bool hasAlarmed(); }; + class AmcrestAPI { protected: + Monitor *parent; + std::string amcrest_response; + CURLM *curl_multi = nullptr; + CURL *Amcrest_handle = nullptr; + static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp); + + public: + AmcrestAPI( Monitor *parent_); + ~AmcrestAPI(); + int API_Connect(); + void WaitForMessage(); + bool Amcrest_Alarmed; + int start_Amcrest(); + }; + + class JanusManager { + protected: + Monitor *parent; + CURL *curl = nullptr; + //helper class for CURL + static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp); + bool Janus_Healthy; + std::string janus_session; + std::string janus_handle; + std::string janus_endpoint; + std::string stream_key; + std::string rtsp_username; + std::string rtsp_password; + std::string rtsp_path; + + public: + JanusManager(Monitor *parent_); + ~JanusManager(); + int add_to_janus(); + int check_janus(); + int remove_from_janus(); + int get_janus_session(); + int get_janus_handle(); + int get_janus_plugin(); + std::string get_stream_key(); + }; // These are read from the DB and thereafter remain unchanged @@ -285,7 +327,6 @@ 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; @@ -438,9 +479,12 @@ protected: std::string diag_path_delta; //ONVIF - bool ONVIF_Trigger_State; //Re-using some variables for Amcrest API support - bool ONVIF_Healthy; - bool ONVIF_Closes_Event; + bool Poll_Trigger_State; + bool Event_Poller_Healthy; + bool Event_Poller_Closes_Event; + + JanusManager *Janus_Manager; + AmcrestAPI *Amcrest_Manager; #ifdef WITH_GSOAP struct soap *soap = nullptr; @@ -452,17 +496,6 @@ protected: void set_credentials(struct soap *soap); #endif - //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(); - int remove_from_janus(); - int get_janus_session(); - std::string janus_session; // Used in check signal uint8_t red_val; @@ -530,11 +563,9 @@ public: return onvif_event_listener; } int check_janus(); //returns 1 for healthy, 0 for success but missing stream, negative for error. -#ifdef WITH_GSOAP - bool OnvifHealthy() { - return ONVIF_Healthy; + bool EventPollerHealthy() { + return Event_Poller_Healthy; } -#endif inline const char *EventPrefix() const { return event_prefix.c_str(); } inline bool Ready() const { if ( image_count >= ready_count ) { @@ -583,7 +614,7 @@ public: void SetVideoWriterStartTime(SystemTimePoint t) { video_store_data->recording = zm::chrono::duration_cast(t.time_since_epoch()); } - + unsigned int GetPreEventCount() const { return pre_event_count; }; int32_t GetImageBufferCount() const { return image_buffer_count; }; State GetState() const { return (State)shared_data->state; } diff --git a/src/zm_monitor_amcrest.cpp b/src/zm_monitor_amcrest.cpp new file mode 100644 index 000000000..04af8734c --- /dev/null +++ b/src/zm_monitor_amcrest.cpp @@ -0,0 +1,126 @@ +// +// ZoneMinder Monitor::AmcrestAPI Class Implementation, $Date$, $Revision$ +// Copyright (C) 2022 Jonathan Bennett +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// + +#include "zm_monitor.h" + + +Monitor::AmcrestAPI::AmcrestAPI(Monitor *parent_) { + parent = parent_; + curl_multi = curl_multi_init(); + start_Amcrest(); +} + +Monitor::AmcrestAPI::~AmcrestAPI() { + if (Amcrest_handle != nullptr) { //potentially clean up the old handle + curl_multi_remove_handle(curl_multi, Amcrest_handle); + curl_easy_cleanup(Amcrest_handle); + } + if (curl_multi != nullptr) curl_multi_cleanup(curl_multi); +} + +int Monitor::AmcrestAPI::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 = parent->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, parent->onvif_username.c_str()); + curl_easy_setopt(Amcrest_handle, CURLOPT_PASSWORD, parent->onvif_password.c_str()); + curl_easy_setopt(Amcrest_handle, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); + curl_error = curl_multi_add_handle(curl_multi, Amcrest_handle); + if (curl_error != CURLM_OK) { + 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)) { + parent->Event_Poller_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; +} + +void Monitor::AmcrestAPI::WaitForMessage() { + 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"); + if (!parent->Poll_Trigger_State) { + Debug(1,"Triggered Event"); + parent->Poll_Trigger_State = TRUE; + } + } else if (amcrest_response.find("action=Stop") != std::string::npos){ + Debug(1, "Triggered off ONVIF"); + parent->Poll_Trigger_State = FALSE; + if (!parent->Event_Poller_Closes_Event) { //If we get a close event, then we know to expect them. + parent->Event_Poller_Closes_Event = TRUE; + Debug(1,"Setting ClosesEvent"); + } + } + amcrest_response.clear(); //We've dealt with the message, need to clear the queue + } + } + return; +} + +size_t Monitor::AmcrestAPI::WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) +{ + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; +} diff --git a/src/zm_monitor_janus.cpp b/src/zm_monitor_janus.cpp new file mode 100644 index 000000000..2fe9b3f26 --- /dev/null +++ b/src/zm_monitor_janus.cpp @@ -0,0 +1,316 @@ +// +// ZoneMinder Monitor::JanusManager Class Implementation, $Date$, $Revision$ +// Copyright (C) 2022 Jonathan Bennett +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// + +#include "zm_monitor.h" + + +Monitor::JanusManager::JanusManager(Monitor *parent_) { //constructor takes care of init and calls add_to + std::string response; + std::size_t pos; + parent = parent_; + if ((config.janus_path != nullptr) && (config.janus_path[0] != '\0')) { + janus_endpoint = config.janus_path; //TODO: strip trailing / + } else { + janus_endpoint = "127.0.0.1:8088/janus"; + } + if (janus_endpoint.back() == '/') janus_endpoint.pop_back(); //remove the trailing slash if present + std::size_t pos2 = parent->path.find("@", pos); + if (pos2 != std::string::npos) { //If we find an @ symbol, we have a username/password. Otherwise, passwordless login. + + std::size_t pos = parent->path.find(":", 7); //Search for the colon, but only after the RTSP:// text. + if (pos == std::string::npos) throw std::runtime_error("Cannot Parse URL for Janus."); //Looks like an invalid url + rtsp_username = parent->path.substr(7, pos-7); + + rtsp_password = parent->path.substr(pos+1, pos2 - pos - 1); + rtsp_path = "RTSP://"; + rtsp_path += parent->path.substr(pos2 + 1); + + } else { + rtsp_username = ""; + rtsp_password = ""; + rtsp_path = parent->path; + } +} + +Monitor::JanusManager::~JanusManager() { + if (janus_session.empty()) get_janus_session(); + if (janus_handle.empty()) get_janus_handle(); + + std::string response; + std::string endpoint; + + std::string postData = "{\"janus\" : \"create\", \"transaction\" : \"randomString\"}"; + //std::size_t pos; + CURLcode res; + + curl = curl_easy_init(); + if(!curl) return; + + endpoint = janus_endpoint; + endpoint += "/"; + endpoint += janus_session; + endpoint += "/"; + endpoint += janus_handle; + + //Assemble our actual request + postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; + postData += "\"request\" : \"destroy\", \"admin_key\" : \""; + postData += config.janus_secret; + postData += "\", \"id\" : "; + postData += std::to_string(parent->id); + postData += "}}"; + + curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + Warning("Libcurl attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res)); + curl_easy_cleanup(curl); + return; + } + + Debug(1, "Removed stream from Janus: %s", response.c_str()); + curl_easy_cleanup(curl); + return; +} + + + +int Monitor::JanusManager::check_janus() { + if (janus_session.empty()) get_janus_session(); + if (janus_handle.empty()) get_janus_handle(); + + std::string response; + std::string endpoint = janus_endpoint; + std::string postData; + //std::size_t pos; + CURLcode res; + + curl = curl_easy_init(); + if(!curl) return -1; + + endpoint += "/"; + endpoint += janus_session; + endpoint += "/"; + endpoint += janus_handle; + + //Assemble our actual request + postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; + postData += "\"request\" : \"info\", \"id\" : "; + postData += std::to_string(parent->id); + postData += "}}"; + + curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); + res = curl_easy_perform(curl); + if (res != CURLE_OK) { //may mean an error code thrown by Janus, because of a bad session + Warning("Attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res)); + curl_easy_cleanup(curl); + janus_session = ""; + janus_handle = ""; + return -1; + } + + curl_easy_cleanup(curl); + Debug(1, "Queried for stream status: %s", response.c_str()); + if (response.find("\"janus\": \"error\"") != std::string::npos) { + if (response.find("No such session") != std::string::npos) { + Warning("Janus Session timed out"); + janus_session = ""; + return -2; + } else if (response.find("No such handle") != std::string::npos) { + Warning("Janus Handle timed out"); + janus_handle = ""; + return -2; + } + } else if (response.find("No such mountpoint") != std::string::npos) { + Warning("Mountpoint Missing"); + return 0; + } + return 1; +} + +int Monitor::JanusManager::add_to_janus() { + if (janus_session.empty()) get_janus_session(); + if (janus_handle.empty()) get_janus_handle(); + + std::string response; + std::string endpoint = janus_endpoint; + + CURLcode res; + + curl = curl_easy_init(); + if (!curl) { + Error("Failed to init curl"); + return -1; + } + + endpoint += "/"; + endpoint += janus_session; + endpoint += "/"; + endpoint += janus_handle; + + //Assemble our actual request + std::string postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; + postData += "\"request\" : \"create\", \"admin_key\" : \""; + postData += config.janus_secret; + postData += "\", \"type\" : \"rtsp\", "; + postData += "\"url\" : \""; + postData += rtsp_path; + if (rtsp_username != "") { + postData += "\", \"rtsp_user\" : \""; + postData += rtsp_username; + postData += "\", \"rtsp_pwd\" : \""; + postData += rtsp_password; + } + postData += "\", \"id\" : "; + postData += std::to_string(parent->id); + if (parent->janus_audio_enabled) postData += ", \"audio\" : true"; + postData += ", \"video\" : true}}"; + Warning("Sending %s to %s", postData.c_str(), endpoint.c_str()); + + curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + Error("Failed to curl_easy_perform adding rtsp stream"); + curl_easy_cleanup(curl); + return -1; + } + if (response.find("\"janus\": \"error\"") != std::string::npos) { + if (response.find("No such session") != std::string::npos) { + Warning("Janus Session timed out"); + janus_session = ""; + return -2; + } else if (response.find("No such handle") != std::string::npos) { + Warning("Janus Handle timed out"); + janus_handle = ""; + return -2; + } + } + //scan for missing session or handle id "No such session" "no such handle" + + Debug(1,"Added stream to Janus: %s", response.c_str()); + curl_easy_cleanup(curl); + return 0; +} + + +size_t Monitor::JanusManager::WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) +{ + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; +} + +/* +void Monitor::JanusManager::generateKey() +{ + const std::string CHARACTERS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + std::random_device random_device; + std::mt19937 generator(random_device()); + std::uniform_int_distribution<> distribution(0, CHARACTERS.size() - 1); + + std::string random_string; + + for (std::size_t i = 0; i < 16; ++i) + { + random_string += CHARACTERS[distribution(generator)]; + } + + stream_key = random_string; +} +*/ + + +int Monitor::JanusManager::get_janus_session() { + janus_session = ""; + std::string endpoint = janus_endpoint; + + std::string response; + + std::string postData = "{\"janus\" : \"create\", \"transaction\" : \"randomString\"}"; + std::size_t pos; + CURLcode res; + curl = curl_easy_init(); + if(!curl) return -1; + + curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + Warning("Libcurl attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res)); + curl_easy_cleanup(curl); + return -1; + } + + pos = response.find("\"id\": "); + if (pos == std::string::npos) + { + curl_easy_cleanup(curl); + return -1; + } + janus_session = response.substr(pos + 6, 16); + curl_easy_cleanup(curl); + return 1; + +} //get_janus_session + +int Monitor::JanusManager::get_janus_handle() { + std::string response = ""; + std::string endpoint = janus_endpoint; + std::size_t pos; + + CURLcode res; + curl = curl_easy_init(); + if(!curl) return -1; + + endpoint += "/"; + endpoint += janus_session; + std::string postData = "{\"janus\" : \"attach\", \"plugin\" : \"janus.plugin.streaming\", \"transaction\" : \"randomString\"}"; + curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); + res = curl_easy_perform(curl); + if (res != CURLE_OK) + { + Warning("Libcurl attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res)); + curl_easy_cleanup(curl); + return -1; + } + + pos = response.find("\"id\": "); + if (pos == std::string::npos) + { + curl_easy_cleanup(curl); + return -1; + } + janus_handle = response.substr(pos + 6, 16); + curl_easy_cleanup(curl); + return 1; +} //get_janus_handle From b3092f2f591f97e453d7b7acf1e5843c5a1bb3c8 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 3 Feb 2022 17:31:37 -0500 Subject: [PATCH 08/21] Add special handling for skip_locked, as it is a checkbox. Don't update REQUEST['Id'] on execute so we can redirect to the original filter. --- web/includes/actions/filter.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/web/includes/actions/filter.php b/web/includes/actions/filter.php index 058a004ed..4f627b0b0 100644 --- a/web/includes/actions/filter.php +++ b/web/includes/actions/filter.php @@ -48,6 +48,7 @@ if (isset($_REQUEST['object']) and ($_REQUEST['object'] == 'filter')) { $_REQUEST['filter']['Query']['sort_field'] = validStr($_REQUEST['filter']['Query']['sort_field']); $_REQUEST['filter']['Query']['sort_asc'] = validStr($_REQUEST['filter']['Query']['sort_asc']); $_REQUEST['filter']['Query']['limit'] = validInt($_REQUEST['filter']['Query']['limit']); + $_REQUEST['filter']['Query']['skip_locked'] = isset($_REQUEST['filter']['Query']['skip_locked']) ? validInt($_REQUEST['filter']['Query']['skip_locked']) : 0; $_REQUEST['filter']['AutoCopy'] = empty($_REQUEST['filter']['AutoCopy']) ? 0 : 1; $_REQUEST['filter']['AutoCopyTo'] = empty($_REQUEST['filter']['AutoCopyTo']) ? 0 : $_REQUEST['filter']['AutoCopyTo']; @@ -80,21 +81,23 @@ if (isset($_REQUEST['object']) and ($_REQUEST['object'] == 'filter')) { $error_message = $filter->get_last_error(); return; } - // We update the request id so that the newly saved filter is auto-selected - $_REQUEST['Id'] = $filter->Id(); + if ($action == 'Save' or $action == 'SaveAs' ) { + // We update the request id so that the newly saved filter is auto-selected + $_REQUEST['Id'] = $filter->Id(); + } } # end if changes if ($action == 'execute') { $filter->execute(); if (count($changes)) { $filter->delete(); - $filter->Id(null); + $filter->Id($_REQUEST['Id']); } } else if ($filter->Background()) { $filter->control('start'); } global $redirect; - $redirect = '?view=filter'.$filter->querystring('filter', '&'); + $redirect = '?view=filter&Id='.$_REQUEST['Id'].$filter->querystring('filter', '&'); } else if ($action == 'control') { if ( $_REQUEST['command'] == 'start' From ac909d404aac7716b55c9c593fabe5b3d2d32142 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 3 Feb 2022 18:19:01 -0500 Subject: [PATCH 09/21] Use the reported move with x=0 y=0 for autostop in addition to old stop movement code --- .../ZoneMinder/lib/ZoneMinder/Control/Netcat.pm | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Control/Netcat.pm b/scripts/ZoneMinder/lib/ZoneMinder/Control/Netcat.pm index f10b7aabb..8043eedad 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Control/Netcat.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Control/Netcat.pm @@ -194,7 +194,6 @@ sub getCamParams { } } -#autoStop #This makes use of the ZoneMinder Auto Stop Timeout on the Control Tab sub autoStop { my $self = shift; @@ -202,13 +201,19 @@ sub autoStop { if ( $autostop ) { Debug('Auto Stop'); - my $cmd = 'onvif/PTZ'; - my $msg = '' . ((%identity) ? authentificationHeader($identity{username}, $identity{password}) : '') . '' . $profileToken . 'truefalse'; - my $content_type = 'application/soap+xml; charset=utf-8; action="http://www.onvif.org/ver20/ptz/wsdl/ContinuousMove"'; usleep($autostop); + + my $cmd = 'onvif/PTZ'; + my $content_type = 'application/soap+xml; charset=utf-8; action="http://www.onvif.org/ver20/ptz/wsdl/ContinuousMove"'; + + my $msg =''.((%identity) ? authentificationHeader($identity{username}, $identity{password}) : '').'' . $profileToken . ''; + $self->sendCmd($cmd, $msg, $content_type); + + # Reported to not work, so superceded by the cmd above + $msg = '' . ((%identity) ? authentificationHeader($identity{username}, $identity{password}) : '') . '' . $profileToken . 'truefalse'; $self->sendCmd($cmd, $msg, $content_type); } -} +} # end sub autoStop # Reset the Camera sub reset { From 69053424cdfa00af7e6ab9fc036149a7cf0487ba Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sun, 6 Feb 2022 19:06:35 -0500 Subject: [PATCH 10/21] When adding a new monitor, ModelId and ManufacturerId are not defined, so handle that --- web/includes/Monitor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/includes/Monitor.php b/web/includes/Monitor.php index 9c7d8d25c..e3599bbf4 100644 --- a/web/includes/Monitor.php +++ b/web/includes/Monitor.php @@ -698,7 +698,7 @@ class Monitor extends ZM_Object { } function Model() { if (!property_exists($this, 'Model')) { - if ($this->{'ModelId'}) { + if (property_exists($this, 'ModelId') and $this->{'ModelId'}) { $this->{'Model'} = Model::find_one(array('Id'=>$this->ModelId())); if (!$this->{'Model'}) $this->{'Model'} = new Model(); @@ -710,7 +710,7 @@ class Monitor extends ZM_Object { } function Manufacturer() { if (!property_exists($this, 'Manufacturer')) { - if ($this->{'ManufacturerId'}) { + if (property_exists($this, 'ManufacturerId') and $this->{'ManufacturerId'}) { $this->{'Manufacturer'} = Manufacturer::find_one(array('Id'=>$this->ManufacturerId())); if (!$this->{'Manufacturer'}) $this->{'Manufacturer'} = new Manufacturer(); From 5078eecdfd867be58c8e9f38c3277bcadbd458e0 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Mon, 7 Feb 2022 12:31:31 -0500 Subject: [PATCH 11/21] Add in get_networks and get_subnets as utilities to parse devices and networks in preparation for scanning/probing --- web/includes/functions.php | 66 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/web/includes/functions.php b/web/includes/functions.php index e346ceb34..bfa4b9746 100644 --- a/web/includes/functions.php +++ b/web/includes/functions.php @@ -2414,4 +2414,70 @@ function i18n() { return implode('-', $string); } + +function get_networks() { + $interfaces = array(); + + exec('ip link', $output, $status); + if ( $status ) { + $html_output = implode('
', $output); + ZM\Error("Unable to list network interfaces, status is '$status'. Output was:

$html_output"); + } else { + foreach ( $output as $line ) { + if ( preg_match('/^\d+: ([[:alnum:]]+):/', $line, $matches ) ) { + if ( $matches[1] != 'lo' ) { + $interfaces[$matches[1]] = $matches[1]; + } else { + ZM\Debug("No match for $line"); + } + } + } + } + $routes = array(); + exec('ip route', $output, $status); + if ( $status ) { + $html_output = implode('
', $output); + ZM\Error("Unable to list network interfaces, status is '$status'. Output was:

$html_output"); + } else { + foreach ( $output as $line ) { + if ( preg_match('/^default via [.[:digit:]]+ dev ([[:alnum:]]+)/', $line, $matches) ) { + $interfaces['default'] = $matches[1]; + } else if ( preg_match('/^([.[:digit:]]+\/[[:digit:]]+) dev ([[:alnum:]]+)/', $line, $matches) ) { + $interfaces[$matches[2]] .= ' ' . $matches[1]; + ZM\Debug("Matched $line: $matches[2] .= $matches[1]"); + } else { + ZM\Debug("Didn't match $line"); + } + } # end foreach line of output + } + return $interfaces; +} + +# Returns an array of subnets like 192.168.1.0/24 for a given interface. +# Will ignore mdns networks. + +function get_subnets($interface) { + $subnets = array(); + exec('ip route', $output, $status); + if ( $status ) { + $html_output = implode('
', $output); + ZM\Error("Unable to list network interfaces, status is '$status'. Output was:

$html_output"); + } else { + foreach ($output as $line) { + if (preg_match('/^([.[:digit:]]+\/[[:digit:]]+) dev ([[:alnum:]]+)/', $line, $matches)) { + if ($matches[1] == '169.254.0.0/16') { + # Ignore mdns + } else if ($matches[2] == $interface) { + $subnets[] = $matches[1]; + } else { + ZM\Debug("Wrong interface $matches[1] != $interface"); + } + } else { + ZM\Debug("Didn't match $line"); + } + } # end foreach line of output + } + return $subnets; +} + ?> From d056f15e54590496c779ec56b49dfd12b62f68d5 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Mon, 7 Feb 2022 12:32:16 -0500 Subject: [PATCH 12/21] Add arp-scan to executable for use in network probing for cameras. --- CMakeLists.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a65f85eb..6337e50c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,6 +83,7 @@ mark_as_advanced( ZM_TARGET_DISTRO ZM_PATH_MAP ZM_PATH_ARP + ZM_PATH_ARP_SCAN ZM_CONFIG_DIR ZM_CONFIG_SUBDIR ZM_SYSTEMD @@ -145,6 +146,8 @@ set(ZM_PATH_MAP "/dev/shm" CACHE PATH "Location to save mapped memory files, default: /dev/shm") set(ZM_PATH_ARP "" CACHE PATH "Full path to compatible arp binary. Leave empty for automatic detection.") +set(ZM_PATH_ARP_SCAN "" CACHE PATH + "Full path to compatible scan_arp binary. Leave empty for automatic detection.") set(ZM_CONFIG_DIR "/${CMAKE_INSTALL_SYSCONFDIR}" CACHE PATH "Location of ZoneMinder configuration, default system config directory") set(ZM_CONFIG_SUBDIR "${ZM_CONFIG_DIR}/conf.d" CACHE PATH @@ -641,6 +644,18 @@ if(ZM_PATH_ARP STREQUAL "") endif() endif() +# Find the path to an arp-scan compatible executable +if(ZM_PATH_ARP_SCAN STREQUAL "") + find_program(ARP_SCAN_EXECUTABLE arp-scan) + if(ARP_SCAN_EXECUTABLE) + set(ZM_PATH_ARP_SCAN "${ARP_SCAN_EXECUTABLE}") + mark_as_advanced(ARP_SCAN_EXECUTABLE) + endif() + if(ARP_SCAN_EXECUTABLE-NOTFOUND) + message(WARNING "Unable to find a compatible arp-scan binary. Monitor probe will be less powerful.") + endif() +endif() + # Some variables that zm expects set(ZM_PID "${ZM_RUNDIR}/zm.pid") set(ZM_CONFIG "${ZM_CONFIG_DIR}/zm.conf") From 2b468fa11591851352dfe18a67b199bddad8dd4e Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Mon, 7 Feb 2022 12:32:50 -0500 Subject: [PATCH 13/21] Add policykit rules for arp-scan --- misc/CMakeLists.txt | 4 ++++ misc/com.zoneminder.arp-scan.policy.in | 21 +++++++++++++++++++++ misc/com.zoneminder.arp-scan.rules.in | 7 +++++++ 3 files changed, 32 insertions(+) create mode 100644 misc/com.zoneminder.arp-scan.policy.in create mode 100644 misc/com.zoneminder.arp-scan.rules.in diff --git a/misc/CMakeLists.txt b/misc/CMakeLists.txt index e51636771..c705b63bb 100644 --- a/misc/CMakeLists.txt +++ b/misc/CMakeLists.txt @@ -7,6 +7,8 @@ configure_file(logrotate.conf.in "${CMAKE_CURRENT_BINARY_DIR}/logrotate.conf" @O configure_file(syslog.conf.in "${CMAKE_CURRENT_BINARY_DIR}/syslog.conf" @ONLY) configure_file(com.zoneminder.systemctl.policy.in "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.systemctl.policy" @ONLY) configure_file(com.zoneminder.systemctl.rules.in "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.systemctl.rules" @ONLY) +configure_file(com.zoneminder.arp-scan.policy.in "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.arp-scan.policy" @ONLY) +configure_file(com.zoneminder.arp-scan.rules.in "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.arp-scan.rules" @ONLY) configure_file(zoneminder.service.in "${CMAKE_CURRENT_BINARY_DIR}/zoneminder.service" @ONLY) configure_file(zoneminder-tmpfiles.conf.in "${CMAKE_CURRENT_BINARY_DIR}/zoneminder-tmpfiles.conf" @ONLY) configure_file(zoneminder.desktop.in "${CMAKE_CURRENT_BINARY_DIR}/zoneminder.desktop" @ONLY) @@ -19,6 +21,8 @@ configure_file(zm-sudo.in "${CMAKE_CURRENT_BINARY_DIR}/zm-sudo" @ONLY) if(WITH_SYSTEMD) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.systemctl.policy" DESTINATION "${PC_POLKIT_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/polkit-1/actions") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.systemctl.rules" DESTINATION "${PC_POLKIT_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/polkit-1/rules.d") + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.arp-scan.policy" DESTINATION "${PC_POLKIT_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/polkit-1/actions") + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.arp-scan.rules" DESTINATION "${PC_POLKIT_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/polkit-1/rules.d") endif() install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zoneminder.desktop" DESTINATION "${CMAKE_INSTALL_DATADIR}/applications") diff --git a/misc/com.zoneminder.arp-scan.policy.in b/misc/com.zoneminder.arp-scan.policy.in new file mode 100644 index 000000000..7b5b6043e --- /dev/null +++ b/misc/com.zoneminder.arp-scan.policy.in @@ -0,0 +1,21 @@ + + + + + The ZoneMinder Project + http://www.zoneminder.com/ + + + Allow the ZoneMinder webuser to run arp-scan + The ZoneMinder webuser is trusted to run arp-scan + + yes + yes + yes + + /usr/sbin/arp-scan + + + diff --git a/misc/com.zoneminder.arp-scan.rules.in b/misc/com.zoneminder.arp-scan.rules.in new file mode 100644 index 000000000..74ac25af0 --- /dev/null +++ b/misc/com.zoneminder.arp-scan.rules.in @@ -0,0 +1,7 @@ +polkit.addRule(function(action, subject) { + if (action.id == "com.zoneminder.policykit.pkexec.run-arp-scan" && + subject.user != "@WEB_USER@") { + return polkit.Result.NO; + } + +}); From 2c23e18ea0071268b376b128c4fdd5a8625bbba1 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Mon, 7 Feb 2022 12:33:05 -0500 Subject: [PATCH 14/21] Add system path for arp-scan --- conf.d/01-system-paths.conf.in | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conf.d/01-system-paths.conf.in b/conf.d/01-system-paths.conf.in index 277f63b70..523a8c0c8 100644 --- a/conf.d/01-system-paths.conf.in +++ b/conf.d/01-system-paths.conf.in @@ -47,5 +47,9 @@ ZM_PATH_SWAP=@ZM_TMPDIR@ # ZoneMinder will find the arp binary automatically on most systems ZM_PATH_ARP="@ZM_PATH_ARP@" +# Full path to optional arp-scan binary +# ZoneMinder will find the arp-scan binary automatically on most systems +ZM_PATH_ARP_SCAN="@ZM_PATH_ARP_SCAN@" + #Full path to shutdown binary ZM_PATH_SHUTDOWN="@ZM_PATH_SHUTDOWN@" From 33473eac6aa4797d50d456292c6699f80b356caf Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Mon, 7 Feb 2022 12:33:53 -0500 Subject: [PATCH 15/21] Add arp-scan as a tool for getting list of devices on network. Add an interface specifier to monitor probe just like on onvif-probe. Rough in support for Hikvision cameras --- web/skins/classic/views/js/monitorprobe.js | 4 + web/skins/classic/views/monitorprobe.php | 128 ++++++++++++++++++--- web/skins/classic/views/onvifprobe.php | 37 +----- 3 files changed, 121 insertions(+), 48 deletions(-) diff --git a/web/skins/classic/views/js/monitorprobe.js b/web/skins/classic/views/js/monitorprobe.js index 96f0c701e..1ecd93d23 100644 --- a/web/skins/classic/views/js/monitorprobe.js +++ b/web/skins/classic/views/js/monitorprobe.js @@ -11,3 +11,7 @@ function configureButtons( element ) { var form = element.form; form.saveBtn.disabled = (form.probe.selectedIndex==0); } + +function changeInterface(element) { + element.form.submit(); +} diff --git a/web/skins/classic/views/monitorprobe.php b/web/skins/classic/views/monitorprobe.php index 7bb15a68d..f9d47fcad 100644 --- a/web/skins/classic/views/monitorprobe.php +++ b/web/skins/classic/views/monitorprobe.php @@ -194,6 +194,21 @@ function probeActi($ip) { return $camera; } +function probeHikvision($ip) { + $url = 'rtsp://admin:password@'.$ip.':554/Streaming/Channels/101?transportmode=unicast'; + $camera = array( + 'model' => 'Unknown Hikvision Camera', + 'monitor' => array( + 'Type' => 'FFmpeg', + 'Path' => $url, + 'Colours' => 4, + 'Width' => 1920, + 'Height' => 1080, + ), + ); + return $camera; +} + function probeVivotek($ip) { $url = 'http://'.$ip.'/cgi-bin/viewer/getparam.cgi'; $camera = array( @@ -244,20 +259,60 @@ function probeWansview($ip) { return $camera; } -function probeNetwork() { - $cameras = array(); +function get_arp_results() { + $results = array(); $arp_command = ZM_PATH_ARP; $result = explode(' ', $arp_command); if ( !is_executable($result[0]) ) { ZM\Error('ARP compatible binary not found or not executable by the web user account. Verify ZM_PATH_ARP points to a valid arp tool.'); - return $cameras; + return $results; + } + if (count($result)==1) { + $arp_command .= ' -n'; } $result = exec(escapeshellcmd($arp_command), $output, $status); - if ( $status ) { + if ($status) { ZM\Error("Unable to probe network cameras, status is '$status'"); - return $cameras; + return $results; } + foreach ($output as $line) { + if ( !preg_match('/(\d+\.\d+\.\d+\.\d+).*(([0-9a-f]{2}:){5})/', $line, $matches) ) { + ZM\Debug("Didn't match preg $line"); + continue; + } + $results[$matches[2]] = $matches[1]; // results[mac] = ip + } + return $results; +} + +function get_arp_scan_results($network) { + ZM\Debug("arp-scanning $network"); + $results = array(); + $arp_scan_command = ZM_PATH_ARP_SCAN; + $result = explode(' ', $arp_scan_command); + if (!is_executable($result[0])) { + ZM\Error('arp-scan compatible binary not found or not executable by the web user account. Verify ZM_PATH_ARP_SCAN points to a valid arp-scan tool.'); + return $results; + } + $arp_scan_command = '/usr/bin/pkexec '.ZM_PATH_ARP_SCAN.' '.$network.' 2>&1'; + $result = exec(escapeshellcmd($arp_scan_command), $output, $status); + if ($status) { + ZM\Error("Unable to probe network cameras, command was $arp_scan_command, status is '$status' output: ".implode(PHP_EOL, $output)); + return $results; + } + foreach ($output as $line) { + if (preg_match('/(\d+\.\d+\.\d+\.\d+)\s+(([0-9a-f]{2}:){5})/', $line, $matches)) { + $results[$matches[2]] = $matches[1]; + } else { + ZM\Debug("Didn't match preg $line"); + } + } + return $results; +} + +function probeNetwork() { + $cameras = array(); $monitors = array(); foreach ( dbFetchAll("SELECT `Id`, `Name`, `Host` FROM `Monitors` WHERE `Type` = 'Remote' ORDER BY `Host`") as $monitor ) { @@ -269,25 +324,26 @@ function probeNetwork() { $monitors[gethostbyname($monitor['Host'])] = $monitor; } } + foreach ( dbFetchAll("SELECT `Id`, `Name`, `Path` FROM `Monitors` WHERE `Type` = 'Ffmpeg' ORDER BY `Path`") as $monitor ) { + $url_parts = parse_url($monitor['Path']); + ZM\Debug("Ffmpeg monitor ${url_parts['host']} = ${monitor['Id']} ${monitor['Name']}"); + $monitors[gethostbyname($url_parts['host'])] = $monitor; + } $macBases = array( - '00:40:8c' => array('type'=>'Axis', 'probeFunc'=>'probeAxis'), - '00:80:f0' => array('type'=>'Panasonic','probeFunc'=>'probePana'), '00:0f:7c' => array('type'=>'ACTi','probeFunc'=>'probeACTi'), + '00:40:8c' => array('type'=>'Axis', 'probeFunc'=>'probeAxis'), + '2c:a5:9c' => array('type'=>'Hikvision', 'probeFunc'=>'probeHikvision'), + '00:80:f0' => array('type'=>'Panasonic','probeFunc'=>'probePana'), '00:02:d1' => array('type'=>'Vivotek','probeFunc'=>'probeVivotek'), '7c:dd:90' => array('type'=>'Wansview','probeFunc'=>'probeWansview'), '78:a5:dd' => array('type'=>'Wansview','probeFunc'=>'probeWansview') ); - foreach ( $output as $line ) { - if ( !preg_match('/(\d+\.\d+\.\d+\.\d+).*(([0-9a-f]{2}:){5})/', $line, $matches) ) - continue; - $ip = $matches[1]; - $host = $ip; - $mac = $matches[2]; - //echo "I:$ip, H:$host, M:$mac
"; + foreach ( get_arp_results() as $mac=>$ip ) { $macRoot = substr($mac,0,8); if ( isset($macBases[$macRoot]) ) { + ZM\Debug("Have match for $macRoot ".$macBases[$macRoot]['type']); $macBase = $macBases[$macRoot]; $camera = call_user_func($macBase['probeFunc'], $ip); $sourceDesc = base64_encode(json_encode($camera['monitor'])); @@ -299,8 +355,36 @@ function probeNetwork() { $sourceString .= ' - '.translate('Available'); } $cameras[$sourceDesc] = $sourceString; + } else { + ZM\Debug("No match for $macRoot"); } } # end foreach output line + + if (isset($_REQUEST['interface']) and $_REQUEST['interface']) { + foreach (get_subnets($_REQUEST['interface']) as $network) { + foreach ( get_arp_scan_results($network) as $mac=>$ip ) { + $macRoot = substr($mac,0,8); + ZM\Debug("Got $macRoot from $mac"); + if (isset($macBases[$macRoot])) { + ZM\Debug("Have match for $macRoot $ip ".$macBases[$macRoot]['type']); + $macBase = $macBases[$macRoot]; + $camera = call_user_func($macBase['probeFunc'], $ip); + $sourceDesc = base64_encode(json_encode($camera['monitor'])); + $sourceString = $camera['model'].' @ '.$host; + if (isset($monitors[$ip])) { + $monitor = $monitors[$ip]; + $sourceString .= ' ('.$monitor['Name'].')'; + } else { + $sourceString .= ' - '.translate('Available'); + } + $cameras[$sourceDesc] = $sourceString; + } else { + ZM\Debug("No match for $macRoot"); + } + } # end foreach output line + } # end foreach network + } # end if we have a network specified + return $cameras; } # end function probeNetwork() @@ -324,11 +408,25 @@ xhtmlHeaders(__FILE__, translate('MonitorProbe') );

- + +

+

+'changeInterface') ); + +?> +

'configureButtons')); ?> diff --git a/web/skins/classic/views/onvifprobe.php b/web/skins/classic/views/onvifprobe.php index 754bb50ef..bcc349192 100644 --- a/web/skins/classic/views/onvifprobe.php +++ b/web/skins/classic/views/onvifprobe.php @@ -173,39 +173,10 @@ if ( !isset($_REQUEST['step']) || ($_REQUEST['step'] == '1') ) {

- ', $output); - ZM\Error("Unable to list network interfaces, status is '$status'. Output was:

$html_output"); - } else { - foreach ( $output as $line ) { - if ( preg_match('/^\d+: ([[:alnum:]]+):/', $line, $matches ) ) { - if ( $matches[1] != 'lo' ) { - $interfaces[$matches[1]] = $matches[1]; - } else { - ZM\Debug("No match for $line"); - } - } - } - } - $routes = array(); - exec('ip route', $output, $status); - if ( $status ) { - $html_output = implode('
', $output); - ZM\Error("Unable to list network interfaces, status is '$status'. Output was:

$html_output"); - } else { - foreach ( $output as $line ) { - if ( preg_match('/^default via [.[:digit:]]+ dev ([[:alnum:]]+)/', $line, $matches) ) { - $default_interface = $matches[1]; - } else if ( preg_match('/^([.\/[:digit:]]+) dev ([[:alnum:]]+)/', $line, $matches) ) { - $interfaces[$matches[2]] .= ' ' . $matches[1]; - } - } # end foreach line of output - } + Date: Tue, 8 Feb 2022 09:57:56 -0500 Subject: [PATCH 16/21] Don't need to test for end() because we added our packet to the queue and still have the lock --- src/zm_packetqueue.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/zm_packetqueue.cpp b/src/zm_packetqueue.cpp index 8b831ec2d..e7e58b6e8 100644 --- a/src/zm_packetqueue.cpp +++ b/src/zm_packetqueue.cpp @@ -117,7 +117,8 @@ bool PacketQueue::queuePacket(std::shared_ptr add_packet) { for ( auto it = ++pktQueue.begin(); - it != pktQueue.end() and *it != add_packet; + //it != pktQueue.end() and // can't git end because we added our packet + *it != add_packet; // iterator is incremented by erase ) { std::shared_ptr zm_packet = *it; From 2f5a403fc4a54ecefbb7f832bef7900a08c24baf Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 8 Feb 2022 10:04:19 -0500 Subject: [PATCH 17/21] Handle when no swap is configured --- web/skins/classic/includes/functions.php | 26 +++++++++++++----------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/web/skins/classic/includes/functions.php b/web/skins/classic/includes/functions.php index beb6590e0..6055a5805 100644 --- a/web/skins/classic/includes/functions.php +++ b/web/skins/classic/includes/functions.php @@ -472,19 +472,21 @@ function getRamHTML() { } else if ($mem_used_percent > 90) { $used_class = 'text-warning'; } - $swap_used = $meminfo['SwapTotal'] - $meminfo['SwapFree']; - $swap_used_percent = (int)(100*$swap_used/$meminfo['SwapTotal']); - $swap_class = ''; - if ($swap_used_percent > 95) { - $swap_class = 'text-danger'; - } else if ($swap_used_percent > 90) { - $swap_class = 'text-warning'; - } - $result .= '

'.PHP_EOL; + ''.translate('Memory').': '.$mem_used_percent.'% '; + + if ($meminfo['SwapTotal']) { + $swap_used = $meminfo['SwapTotal'] - $meminfo['SwapFree']; + $swap_used_percent = (int)(100*$swap_used/$meminfo['SwapTotal']); + $swap_class = ''; + if ($swap_used_percent > 95) { + $swap_class = 'text-danger'; + } else if ($swap_used_percent > 90) { + $swap_class = 'text-warning'; + } + $result .= ''.translate('Swap').': '.$swap_used_percent.'% '; + } # end if SwapTotal + $result .= ''.PHP_EOL; return $result; } From 8e689340ce83fdf1b98ba7b1c08fc0e1137724bb Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 8 Feb 2022 10:06:32 -0500 Subject: [PATCH 18/21] Don't need to check for end of queue as we already did that when adding packet to the queue --- src/zm_packetqueue.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zm_packetqueue.cpp b/src/zm_packetqueue.cpp index e7e58b6e8..7daf0503d 100644 --- a/src/zm_packetqueue.cpp +++ b/src/zm_packetqueue.cpp @@ -138,7 +138,7 @@ bool PacketQueue::queuePacket(std::shared_ptr add_packet) { ) { auto iterator_it = *iterators_it; // Have to check each iterator and make sure it doesn't point to the packet we are about to delete - if ((*iterator_it!=pktQueue.end()) and (*(*iterator_it) == zm_packet)) { + if (*(*iterator_it) == zm_packet) { Debug(1, "Bumping IT because it is at the front that we are deleting"); ++(*iterator_it); } From 8c13aa7d3ad1ed6ed504e4f3838b51d8c6d78df9 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 8 Feb 2022 10:10:00 -0500 Subject: [PATCH 19/21] Cleanup LockedPacket, use RAII --- src/zm_packetqueue.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/zm_packetqueue.cpp b/src/zm_packetqueue.cpp index 7daf0503d..d5407b05d 100644 --- a/src/zm_packetqueue.cpp +++ b/src/zm_packetqueue.cpp @@ -123,10 +123,9 @@ bool PacketQueue::queuePacket(std::shared_ptr add_packet) { ) { std::shared_ptr zm_packet = *it; - ZMLockedPacket *lp = new ZMLockedPacket(zm_packet); - if (!lp->trylock()) { + ZMLockedPacket lp(zm_packet); + if (!lp.trylock()) { Warning("Found locked packet when trying to free up video packets. This basically means that decoding is not keeping up."); - delete lp; ++it; continue; } @@ -155,8 +154,6 @@ bool PacketQueue::queuePacket(std::shared_ptr add_packet) { max_video_packet_count, pktQueue.size()); - delete lp; - if (zm_packet->packet.stream_index == video_stream_id) break; } // end while From 2768975f960f338ab405455bdccaf1f6114f4965 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 8 Feb 2022 10:12:29 -0500 Subject: [PATCH 20/21] Only notify one. Anyone waiting is waiting on a lock and only 1 process can get that lock, so only one should try. --- src/zm_packetqueue.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zm_packetqueue.cpp b/src/zm_packetqueue.cpp index d5407b05d..26f19de75 100644 --- a/src/zm_packetqueue.cpp +++ b/src/zm_packetqueue.cpp @@ -161,7 +161,7 @@ bool PacketQueue::queuePacket(std::shared_ptr add_packet) { } // end lock scope // We signal on every packet because someday we may analyze sound Debug(4, "packetqueue queuepacket, unlocked signalling"); - condition.notify_all(); + condition.notify_one(); return true; } // end bool PacketQueue::queuePacket(ZMPacket* zm_packet) From a7dc9d4e36521678d438d7593af1ebce3d6c22d0 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 8 Feb 2022 10:14:00 -0500 Subject: [PATCH 21/21] Implement General::jsonLoad --- scripts/ZoneMinder/lib/ZoneMinder/General.pm | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/scripts/ZoneMinder/lib/ZoneMinder/General.pm b/scripts/ZoneMinder/lib/ZoneMinder/General.pm index b14b08aae..90e7399ce 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/General.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/General.pm @@ -28,6 +28,7 @@ our %EXPORT_TAGS = ( makePath jsonEncode jsonDecode + jsonLoad systemStatus packageControl daemonControl @@ -536,6 +537,23 @@ sub jsonDecode { return $result; } +sub jsonLoad { + my $file = shift; + my $json = undef; + eval { + require File::Slurp; + my $contents = File::Slurp::read_file($file); + if (!$contents) { + Error("No contents for $file"); + return $json; + } + require JSON; + $json = JSON::decode_json($contents); + }; + Error($@) if $@; + return $json; +} + sub parseNameEqualsValueToHash { my %settings; foreach my $line ( split ( /\r?\n/, $_[0] ) ) {