Merge branch 'master' into replace_function_concept

This commit is contained in:
Isaac Connor 2022-02-08 12:07:30 -05:00
commit f66a463574
26 changed files with 1064 additions and 662 deletions

View File

@ -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")

View File

@ -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@"

View File

@ -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")

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<vendor>The ZoneMinder Project</vendor>
<vendor_url>http://www.zoneminder.com/</vendor_url>
<action id="com.zoneminder.policykit.pkexec.run-arp-scan">
<description>Allow the ZoneMinder webuser to run arp-scan</description>
<message>The ZoneMinder webuser is trusted to run arp-scan</message>
<defaults>
<allow_any>yes</allow_any>
<allow_inactive>yes</allow_inactive>
<allow_active>yes</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">/usr/sbin/arp-scan</annotate>
</action>
</policyconfig>

View File

@ -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;
}
});

View File

@ -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 = '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">' . ((%identity) ? authentificationHeader($identity{username}, $identity{password}) : '') . '<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><Stop xmlns="http://www.onvif.org/ver20/ptz/wsdl"><ProfileToken>' . $profileToken . '</ProfileToken><PanTilt>true</PanTilt><Zoom>false</Zoom></Stop></s:Body></s:Envelope>';
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 ='<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">'.((%identity) ? authentificationHeader($identity{username}, $identity{password}) : '').'<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><ContinuousMove xmlns="http://www.onvif.org/ver20/ptz/wsdl"><ProfileToken>' . $profileToken . '</ProfileToken><Velocity><PanTilt x="0" y="0" xmlns="http://www.onvif.org/ver10/schema"/></Velocity></ContinuousMove></s:Body></s:Envelope>';
$self->sendCmd($cmd, $msg, $content_type);
# Reported to not work, so superceded by the cmd above
$msg = '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">' . ((%identity) ? authentificationHeader($identity{username}, $identity{password}) : '') . '<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><Stop xmlns="http://www.onvif.org/ver20/ptz/wsdl"><ProfileToken>' . $profileToken . '</ProfileToken><PanTilt>true</PanTilt><Zoom>false</Zoom></Stop></s:Body></s:Envelope>';
$self->sendCmd($cmd, $msg, $content_type);
}
}
} # end sub autoStop
# Reset the Camera
sub reset {

View File

@ -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] ) ) {

View File

@ -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");
}
}

View File

@ -33,6 +33,9 @@ set(ZM_BIN_SRC_FILES
zm_libvnc_camera.cpp
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

View File

@ -141,175 +141,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<intmax_t>(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<intmax_t>(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<intmax_t>(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),
@ -329,7 +161,7 @@ Monitor::Monitor()
//user
//pass
//path
//device
//device
palette(0),
channel(0),
format(0),
@ -434,7 +266,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
@ -1101,21 +935,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";
@ -1141,7 +970,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
@ -1153,17 +982,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());
@ -1260,12 +1081,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() {
@ -1834,38 +1655,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
@ -1874,7 +1670,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);
@ -1891,16 +1687,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");
}
}
@ -1911,11 +1707,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));
@ -1970,8 +1764,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;
@ -1979,10 +1773,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
@ -3292,8 +3086,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<PollThread>(this);
} else {
@ -3343,10 +3136,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");
@ -3359,11 +3148,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) {
@ -3500,289 +3289,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;
}

View File

@ -121,7 +121,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,
@ -187,9 +187,9 @@ protected:
/*
** 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;
@ -290,7 +290,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
@ -324,7 +366,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;
@ -478,9 +519,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;
@ -492,17 +536,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;
@ -573,11 +606,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) {
@ -654,7 +685,7 @@ public:
void SetVideoWriterStartTime(SystemTimePoint t) {
video_store_data->recording = zm::chrono::duration_cast<timeval>(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; }

126
src/zm_monitor_amcrest.cpp Normal file
View File

@ -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;
}

316
src/zm_monitor_janus.cpp Normal file
View File

@ -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

View File

@ -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 <sys/stat.h>
#if ZM_MEM_MAPPED
#include <sys/mman.h>
#include <fcntl.h>
#else // ZM_MEM_MAPPED
#include <sys/ipc.h>
#include <sys/shm.h>
#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<intmax_t>(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<intmax_t>(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<intmax_t>(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;
}

View File

@ -117,15 +117,15 @@ bool PacketQueue::queuePacket(std::shared_ptr<ZMPacket> 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 <ZMPacket>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;
}
@ -137,7 +137,7 @@ bool PacketQueue::queuePacket(std::shared_ptr<ZMPacket> 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);
}
@ -154,8 +154,6 @@ bool PacketQueue::queuePacket(std::shared_ptr<ZMPacket> add_packet) {
max_video_packet_count,
pktQueue.size());
delete lp;
if (zm_packet->packet.stream_index == video_stream_id)
break;
} // end while
@ -163,7 +161,7 @@ bool PacketQueue::queuePacket(std::shared_ptr<ZMPacket> 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)

View File

@ -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';

View File

@ -759,7 +759,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();
@ -771,7 +771,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();

View File

@ -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)) {

View File

@ -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'

View File

@ -2414,4 +2414,70 @@ function i18n() {
return implode('-', $string);
}
function get_networks() {
$interfaces = array();
exec('ip link', $output, $status);
if ( $status ) {
$html_output = implode('<br/>', $output);
ZM\Error("Unable to list network interfaces, status is '$status'. Output was:<br/><br/>$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('<br/>', $output);
ZM\Error("Unable to list network interfaces, status is '$status'. Output was:<br/><br/>$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('<br/>', $output);
ZM\Error("Unable to list network interfaces, status is '$status'. Output was:<br/><br/>$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;
}
?>

View File

@ -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 .= ' <li id="getRamHTML" class="nav-item dropdown mx-2">'.
'<span class="'.$used_class.'" title="' .human_filesize($mem_used). ' of ' .human_filesize($meminfo['MemTotal']). '">'.translate('Memory').': '.$mem_used_percent.'%</span> '.
'<span class="'.$swap_class.'" title="' .human_filesize($swap_used). ' of ' .human_filesize($meminfo['SwapTotal']). '">'.translate('Swap').': '.$swap_used_percent.'%</span> '.
'</li>'.PHP_EOL;
'<span class="'.$used_class.'" title="' .human_filesize($mem_used). ' of ' .human_filesize($meminfo['MemTotal']). '">'.translate('Memory').': '.$mem_used_percent.'%</span> ';
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 .= '<span class="'.$swap_class.'" title="' .human_filesize($swap_used). ' of ' .human_filesize($meminfo['SwapTotal']). '">'.translate('Swap').': '.$swap_used_percent.'%</span> ';
} # end if SwapTotal
$result .= '</li>'.PHP_EOL;
return $result;
}

View File

@ -204,7 +204,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

View File

@ -87,7 +87,7 @@ var eventDataStrings = {
Emailed: '<?php echo translate('Emailed') ?>'
};
var monitorUrl = '<?php echo $Event->Storage()->Server()->UrlToIndex(); ?>';
var monitorUrl = '<?php echo $Event->Server()->UrlToIndex(); ?>';
var filterQuery = '<?php echo isset($filterQuery)?validJsStr(htmlspecialchars_decode($filterQuery)):'' ?>';
var sortQuery = '<?php echo isset($sortQuery)?validJsStr(htmlspecialchars_decode($sortQuery)):'' ?>';

View File

@ -11,3 +11,7 @@ function configureButtons( element ) {
var form = element.form;
form.saveBtn.disabled = (form.probe.selectedIndex==0);
}
function changeInterface(element) {
element.form.submit();
}

View File

@ -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<br/>";
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') );
<h2><?php echo translate('MonitorProbe') ?></h2>
<div id="content">
<form name="contentForm" id="contentForm" method="get" action="?">
<input type="hidden" name="view" value="none"/>
<input type="hidden" name="mid" value="<?php echo isset($_REQUEST['mid'])?validNum($_REQUEST['mid']):'' ?>"/>
<input type="hidden" name="view" value="monitorprobe"/>
<p>
<?php echo translate('MonitorProbeIntro') ?>
</p>
<p><label for="interface"><?php echo translate('Interface') ?></label>
<?php
$interfaces = array('', 'select');
$interfaces += get_networks();
$default_interface = $interfaces['default'];
unset($interfaces['default']);
echo htmlSelect('interface', $interfaces,
(isset($_REQUEST['interface']) ? $_REQUEST['interface'] : $default_interface),
array('data-on-change-this'=>'changeInterface') );
?>
</p>
<p>
<label for="probe"><?php echo translate('DetectedCameras') ?></label>
<?php echo htmlSelect('probe', $cameras, null, array('data-on-change-this'=>'configureButtons')); ?>

View File

@ -173,39 +173,10 @@ if ( !isset($_REQUEST['step']) || ($_REQUEST['step'] == '1') ) {
<?php echo translate('OnvifProbeIntro') ?>
</p>
<p><label for="interface"><?php echo translate('Interface') ?></label>
<?php
$interfaces = array();
$default_interface = null;
exec('ip link', $output, $status);
if ( $status ) {
$html_output = implode('<br/>', $output);
ZM\Error("Unable to list network interfaces, status is '$status'. Output was:<br/><br/>$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('<br/>', $output);
ZM\Error("Unable to list network interfaces, status is '$status'. Output was:<br/><br/>$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
}
<?php
$interfaces = get_networks();
$default_interface = $interfaces['default'];
unset($interfaces['default']);
echo htmlSelect('interface', $interfaces,
(isset($_REQUEST['interface']) ? $_REQUEST['interface'] : $default_interface),