Merge branch 'master' into reports

pull/3588/head
Isaac Connor 2022-08-30 19:40:14 +02:00
commit e24432ee4d
90 changed files with 4285 additions and 905 deletions

View File

@ -13,6 +13,10 @@ jobs:
dist: buster
- os: debian
dist: bullseye
- os: ubuntu
dist: bionic
- os: ubuntu
dist: focal
runs-on: ubuntu-latest
steps:

View File

@ -439,8 +439,28 @@ if(NOT ZM_NO_PRCE)
endif()
endif()
if(NOT ZM_NO_MQTT)
find_package(Mosquitto)
if(MOSQUITTO_FOUND)
include_directories(${MOSQUITTO_INCLUDE_DIRS})
list(APPEND ZM_BIN_LIBS "${MOSQUITTO_LIBRARIES}")
set(optlibsfound "${optlibsfound} Mosquitto")
else()
set(optlibsnotfound "${optlibsnotfound} Mosquitto")
endif (MOSQUITTO_FOUND)
find_package(Mosquittopp)
if(MOSQUITTOPP_FOUND)
include_directories(${MOSQUITTOPP_INCLUDE_DIRS})
list(APPEND ZM_BIN_LIBS "${MOSQUITTOPP_LIBRARIES}")
set(optlibsfound "${optlibsfound} Mosquittopp")
else()
set(optlibsnotfound "${optlibsnotfound} Mosquittopp")
endif (MOSQUITTOPP_FOUND)
endif()
# mysqlclient (using find_library and find_path)
find_library(MYSQLCLIENT_LIBRARIES mysqlclient PATH_SUFFIXES mysql)
find_library(MYSQLCLIENT_LIBRARIES mysqlclient PATH_SUFFIXES mysql)
if(MYSQLCLIENT_LIBRARIES)
set(HAVE_LIBMYSQLCLIENT 1)
list(APPEND ZM_BIN_LIBS "${MYSQLCLIENT_LIBRARIES}")

View File

@ -0,0 +1,28 @@
# - Find libmosquitto
# Find the native libmosquitto includes and libraries
#
# MOSQUITTO_INCLUDE_DIR - where to find mosquitto.h, etc.
# MOSQUITTO_LIBRARIES - List of libraries when using libmosquitto.
# MOSQUITTO_FOUND - True if libmosquitto found.
if(MOSQUITTO_INCLUDE_DIR)
# Already in cache, be silent
set(MOSQUITTO_FIND_QUIETLY TRUE)
endif(MOSQUITTO_INCLUDE_DIR)
find_path(MOSQUITTO_INCLUDE_DIR mosquitto.h)
find_library(MOSQUITTO_LIBRARY NAMES libmosquitto mosquitto)
# Handle the QUIETLY and REQUIRED arguments and set MOSQUITTO_FOUND to TRUE if
# all listed variables are TRUE.
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(MOSQUITTO DEFAULT_MSG MOSQUITTO_LIBRARY MOSQUITTO_INCLUDE_DIR)
if(MOSQUITTO_FOUND)
set(MOSQUITTO_LIBRARIES ${MOSQUITTO_LIBRARY})
else(MOSQUITTO_FOUND)
set(MOSQUITTO_LIBRARIES)
endif(MOSQUITTO_FOUND)
mark_as_advanced(MOSQUITTO_INCLUDE_DIR MOSQUITTO_LIBRARY)

View File

@ -0,0 +1,28 @@
# - Find libmosquitto
# Find the native libmosquitto includes and libraries
#
# MOSQUITTOPP_INCLUDE_DIR - where to find mosquitto.h, etc.
# MOSQUITTOPP_LIBRARIES - List of libraries when using libmosquitto.
# MOSQUITTOPP_FOUND - True if libmosquitto found.
if(MOSQUITTOPP_INCLUDE_DIR)
# Already in cache, be silent
set(MOSQUITTOPP_FIND_QUIETLY TRUE)
endif(MOSQUITTOPP_INCLUDE_DIR)
find_path(MOSQUITTOPP_INCLUDE_DIR mosquitto.h)
find_library(MOSQUITTOPP_LIBRARY NAMES libmosquittopp mosquittopp)
# Handle the QUIETLY and REQUIRED arguments and set MOSQUITTO_FOUND to TRUE if
# all listed variables are TRUE.
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(MOSQUITTOPP DEFAULT_MSG MOSQUITTOPP_LIBRARY MOSQUITTOPP_INCLUDE_DIR)
if(MOSQUITTOPP_FOUND)
set(MOSQUITTOPP_LIBRARIES ${MOSQUITTOPP_LIBRARY})
else(MOSQUITTOPP_FOUND)
set(MOSQUITTOPP_LIBRARIES)
endif(MOSQUITTOPP_FOUND)
mark_as_advanced(MOSQUITTOPP_INCLUDE_DIR MOSQUITTOPP_LIBRARY)

View File

@ -465,6 +465,8 @@ CREATE TABLE `Monitors` (
`Decoding` enum('None','Ondemand','KeyFrames','KeyFrames+Ondemand', 'Always') NOT NULL default 'Always',
`JanusEnabled` BOOLEAN NOT NULL default false,
`JanusAudioEnabled` BOOLEAN NOT NULL default false,
`Janus_Profile_Override` VARCHAR(30) NOT NULL DEFAULT '',
`Janus_Use_RTSP_Restream` BOOLEAN NOT NULL default false,
`LinkedMonitors` varchar(255),
`Triggers` set('X10') NOT NULL default '',
`EventStartCommand` VARCHAR(255) NOT NULL DEFAULT '',
@ -560,6 +562,8 @@ CREATE TABLE `Monitors` (
`RTSPServer` BOOLEAN NOT NULL DEFAULT FALSE,
`RTSPStreamName` varchar(255) NOT NULL default '',
`Importance` enum('Normal','Less','Not') NOT NULL default 'Normal',
`MQTT_Enabled` BOOLEAN NOT NULL DEFAULT false,
`MQTT_Subscriptions` varchar(255) default '',
PRIMARY KEY (`Id`)
) ENGINE=@ZM_MYSQL_ENGINE@;
@ -667,7 +671,7 @@ CREATE TABLE `Stats` (
`MaxY` smallint(5) unsigned NOT NULL default '0',
`Score` smallint(5) unsigned NOT NULL default '0',
PRIMARY KEY (`Id`),
KEY `EventId` (`EventId`),
KEY `EventId_ZoneId` (`EventId`, `ZoneId`),
KEY `MonitorId` (`MonitorId`),
KEY `ZoneId` (`ZoneId`)
) ENGINE=@ZM_MYSQL_ENGINE@;

37
db/zm_update-1.37.20.sql Normal file
View File

@ -0,0 +1,37 @@
--
-- Update Monitors Table to include Janus_Profile_Override
--
SELECT 'Checking for Janus_Profile_Override in Monitors';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'Monitors'
AND table_schema = DATABASE()
AND column_name = 'Janus_Profile_Override'
) > 0,
"SELECT 'Column Janus_Profile_Override already exists in Monitors'",
"ALTER TABLE Monitors ADD Janus_Profile_Override varchar(30) DEFAULT '' AFTER `JanusAudioEnabled`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;
--
-- Update Monitors Table to include Janus_Use_RTSP_Restream
--
SELECT 'Checking for Janus_Use_RTSP_Restream in Monitors';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'Monitors'
AND table_schema = DATABASE()
AND column_name = 'Janus_Use_RTSP_Restream'
) > 0,
"SELECT 'Column Janus_Use_RTSP_Restream already exists in Monitors'",
"ALTER TABLE Monitors ADD Janus_Use_RTSP_Restream BOOLEAN NOT NULL DEFAULT false AFTER `Janus_Profile_Override`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;

View File

@ -1,25 +1,37 @@
--
-- This adds the Reports Table
-- Update Monitors table to have a MQTT_Enabled Column
--
SELECT 'Checking for MQTT_Enabled in Monitors';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.TABLES
WHERE table_name = 'Reports'
AND table_schema = DATABASE()
) > 0,
"SELECT 'Reports table exists'",
"
CREATE TABLE Reports (
Id INT(10) UNSIGNED auto_increment,
Name varchar(30),
FilterId int(10) UNSIGNED,
`StartDateTime` datetime default NULL,
`EndDateTime` datetime default NULL,
`Interval` INT(10) UNSIGNED,
PRIMARY KEY(Id)
) ENGINE=InnoDB;"
));
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'Monitors'
AND table_schema = DATABASE()
AND column_name = 'MQTT_Enabled'
) > 0,
"SELECT 'Column MQTT_Enabled already exists in Monitors'",
"ALTER TABLE Monitors ADD COLUMN `MQTT_Enabled` BOOLEAN NOT NULL DEFAULT false AFTER `Importance`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;
--
-- Update Monitors table to have a MQTT_Subscriptions Column
--
SELECT 'Checking for MQTT_Subscriptions in Monitors';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'Monitors'
AND table_schema = DATABASE()
AND column_name = 'MQTT_Subscriptions'
) > 0,
"SELECT 'Column MQTT_Subscriptions already exists in Monitors'",
"ALTER TABLE Monitors ADD COLUMN `MQTT_Subscriptions` varchar(255) NOT NULL default '' AFTER `MQTT_Enabled`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;

25
db/zm_update-1.37.22.sql Normal file
View File

@ -0,0 +1,25 @@
--
-- This adds the Reports Table
--
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.TABLES
WHERE table_name = 'Reports'
AND table_schema = DATABASE()
) > 0,
"SELECT 'Reports table exists'",
"
CREATE TABLE Reports (
Id INT(10) UNSIGNED auto_increment,
Name varchar(30),
FilterId int(10) UNSIGNED,
`StartDateTime` datetime default NULL,
`EndDateTime` datetime default NULL,
`Interval` INT(10) UNSIGNED,
PRIMARY KEY(Id)
) ENGINE=InnoDB;"
));
PREPARE stmt FROM @s;
EXECUTE stmt;

View File

@ -37,7 +37,7 @@
%global _hardened_build 1
Name: zoneminder
Version: 1.37.19
Version: 1.37.21
Release: 1%{?dist}
Summary: A camera monitoring and analysis tool
Group: System Environment/Daemons

View File

@ -32,6 +32,7 @@ Build-Depends: debhelper (>= 11), sphinx-doc, python3-sphinx, dh-linktree, dh-ap
,libvncserver-dev
,libjwt-gnutls-dev|libjwt-dev
,libgsoap-dev
,libmosquittopp-dev
Standards-Version: 4.5.0
Homepage: https://www.zoneminder.com/
@ -78,6 +79,7 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends}
,libvncclient1|libvncclient0
,libjwt-gnutls0|libjwt0
,libgsoap-2.8.117|libgsoap-2.8.104|libgsoap-2.8.91|libgsoap-2.8.75|libgsoap-2.8.60|libgsoap10
,libmosquittopp1
Recommends: ${misc:Recommends}
,libapache2-mod-php | php-fpm
,default-mysql-server | mariadb-server | virtual-mysql-server

View File

@ -162,6 +162,12 @@ our %types = (
pattern => qr|^([a-zA-Z0-9_.-]+)\@([a-zA-Z0-9_.-]+)$|,
format => q( $1\@$2 )
},
password => {
db_type => 'password',
hint => 'password',
pattern => qr|^(.+)$|,
format => q($1)
},
timezone => {
db_type => 'string',
hint => 'America/Toronto',
@ -978,13 +984,12 @@ our @options = (
{
name => 'ZM_MIN_RTSP_PORT',
default => '',
description => 'Start of port range to contact for RTSP streaming video.',
description => 'Port used for RTSP streaming video.',
help => q`
The beginng of a port range that will be used to offer
The port that will be used to offer
RTSP streaming of live captured video.
Each monitor will use this value plus the Monitor Id to stream
content. So a value of 2000 here will cause a stream for Monitor 1 to
hit port 2001.`,
Each camera to be streamed must be enabled
under its misc tab.`,
type => $types{integer},
category => 'network',
},
@ -3834,6 +3839,93 @@ our @options = (
type => $types{string},
category => 'config',
},
{
name => 'ZM_MQTT_HOSTNAME',
default => 'mqtt.zoneminder.com',
description => 'MQTT broker hostname',
help => 'MQTT uses a central server to send/receive messages. This is the hostname or ip address of the server you wish to use.',
type => $types{hostname},
category => 'MQTT',
},
{
name => 'ZM_MQTT_PORT',
default => '1883',
description => 'MQTT broker port',
help => 'MQTT uses a central server to send/receive messages. This is the port to connect to.',
type => $types{integer},
category => 'MQTT',
},
{
name => 'ZM_MQTT_USERNAME',
default => '',
description => 'MQTT broker username',
help => 'MQTT uses a central server to send/receive messages. This is the username to authenticate with.',
type => $types{string},
category => 'MQTT',
},
{
name => 'ZM_MQTT_PASSWORD',
default => '',
description => 'MQTT broker password',
help => 'MQTT uses a central server to send/receive messages. This is the password to authenticate with.',
type => $types{password},
category => 'MQTT',
},
{
name => 'ZM_MQTT_TOPIC_PREFIX',
default => 'ZoneMinder',
description => 'MQTT topic prefix',
help => 'MQTT each message generated by ZoneMinder will start with this. For example /ZoneMinder/available.',
type => $types{string},
category => 'MQTT',
},
# Add options for Alarm Server
{
name => 'ZM_OPT_USE_ALARMSERVER',
default => 'no',
description => 'Enable NETSurveillance WEB Camera ALARM SERVER',
help => q`
Alarm Server that works with cameras that use Netsurveillance Web Server,
and has the Alarm Server option it receives alarms sent by this cameras
(once enabled), and pass to Zoneminder the events.
It requires pyzm installed, visit https://pyzm.readthedocs.io/en/latest/
for installation instructions.
`,
type => $types{boolean},
category => 'system',
},
{
name => 'ZM_OPT_ALS_LOGENTRY',
default => 'no',
description => 'Makes ALARM SERVER create a log entry in ZoneMinder on Human Detected',
help => '',
type => $types{boolean},
category => 'system',
},
{
name => 'ZM_OPT_ALS_ALARM',
default => 'no',
description => 'Send the Human Detected alarm from ALARM SERVER to ZoneMinder, It does not work along with OPT_ALS_TRIGGEREVENT',
help => '',
type => $types{boolean},
category => 'system',
},
{
name => 'ZM_OPT_ALS_TRIGGEREVENT',
default => 'no',
description => 'Trigger an event on Human Detected alarm from ALARM SERVER to ZoneMinder. Requires the zmTrigger option Enabled',
help => '',
type => $types{boolean},
category => 'system',
},
{
name => 'ZM_OPT_ALS_PORT',
default => '15002',
description => 'Port Number to receive alarms from Alarm Server',
help => '',
type => $types{integer},
category => 'system',
},
);
our %options_hash = map { ( $_->{name}, $_ ) } @options;

View File

@ -163,57 +163,81 @@ sub cameraReset {
sub moveConUp {
my $self = shift;
my $params = shift;
my $panspeed = 0; # purely moving vertically
my $tiltspeed = $self->getParam( $params, 'tiltspeed', 30 );
Debug('Move Up');
my $cmd = '/axis-cgi/com/ptz.cgi?move=up';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd($cmd);
}
sub moveConDown {
my $self = shift;
my $params = shift;
my $panspeed = 0; # purely moving vertically
my $tiltspeed = $self->getParam( $params, 'tiltspeed', 30 ) * -1 ;
Debug('Move Down');
my $cmd = '/axis-cgi/com/ptz.cgi?move=down';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd($cmd);
}
sub moveConLeft {
my $self = shift;
my $params = shift;
my $panspeed = $self->getParam( $params, 'panspeed', 30 ) * -1 ;
my $tiltspeed = 0; # purely moving horizontally
Debug('Move Left');
my $cmd = '/axis-cgi/com/ptz.cgi?move=left';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd($cmd);
}
sub moveConRight {
my $self = shift;
my $params = shift;
my $panspeed = $self->getParam( $params, 'panspeed', 30 );
my $tiltspeed = 0; # purely moving horizontally
Debug('Move Right');
my $cmd = '/axis-cgi/com/ptz.cgi?move=right';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd($cmd);
}
sub moveConUpRight {
my $self = shift;
my $params = shift;
my $panspeed = $self->getParam( $params, 'panspeed', 30 );
my $tiltspeed = $self->getParam( $params, 'tiltspeed', 30 );
Debug('Move Up/Right');
my $cmd = '/axis-cgi/com/ptz.cgi?move=upright';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd($cmd);
}
sub moveConUpLeft {
my $self = shift;
my $params = shift;
my $panspeed = $self->getParam( $params, 'panspeed', 30 ) * -1;
my $tiltspeed = $self->getParam( $params, 'tiltspeed', 30 );
Debug('Move Up/Left');
my $cmd = '/axis-cgi/com/ptz.cgi?move=upleft';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd($cmd);
}
sub moveConDownRight {
my $self = shift;
my $params = shift;
my $panspeed = $self->getParam( $params, 'panspeed', 30 );
my $tiltspeed = $self->getParam( $params, 'tiltspeed', 30 ) * -1;
Debug('Move Down/Right');
my $cmd = '/axis-cgi/com/ptz.cgi?move=downright';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd( $cmd );
}
sub moveConDownLeft {
my $self = shift;
my $params = shift;
my $panspeed = $self->getParam( $params, 'panspeed', 30 ) * -1;
my $tiltspeed = $self->getParam( $params, 'tiltspeed', 30 ) * -1;
Debug('Move Down/Left');
my $cmd = '/axis-cgi/com/ptz.cgi?move=downleft';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd($cmd);
}
@ -248,7 +272,7 @@ sub moveRelDown {
sub moveRelLeft {
my $self = shift;
my $params = shift;
my $step = $self->getParam($params, 'panstep');
my $step = abs($self->getParam($params, 'panstep'));
Debug("Step Left $step");
my $cmd = '/axis-cgi/com/ptz.cgi?rpan=-'.$step;
$self->sendCmd($cmd);
@ -276,8 +300,8 @@ sub moveRelUpRight {
sub moveRelUpLeft {
my $self = shift;
my $params = shift;
my $panstep = $self->getParam($params, 'panstep');
my $tiltstep = $self->getParam($params, 'tiltstep');
my $panstep = abs($self->getParam($params, 'panstep'));
my $tiltstep = abs($self->getParam($params, 'tiltstep'));
Debug("Step Up/Left $tiltstep/$panstep");
my $cmd = "/axis-cgi/com/ptz.cgi?rpan=-$panstep&rtilt=$tiltstep";
$self->sendCmd($cmd);
@ -303,6 +327,47 @@ sub moveRelDownLeft {
$self->sendCmd($cmd);
}
sub zoomConTele {
my $self = shift;
my $params = shift;
my $speed = 20;
Debug('Zoom ConTele');
my $cmd = "/axis-cgi/com/ptz.cgi?continuouszoommove=$speed";
$self->sendCmd($cmd);
}
sub zoomConWide {
my $self = shift;
my $params = shift;
#my $step = $self->getParam($params, 'step');
my $speed = -20;
Debug('Zoom ConWide');
my $cmd = "/axis-cgi/com/ptz.cgi?continuouszoommove=$speed";
$self->sendCmd($cmd);
}
sub zoomStop {
my $self = shift;
my $params = shift;
my $speed = 0;
Debug('Zoom Stop');
my $cmd = "/axis-cgi/com/ptz.cgi?continuouszoommove=$speed";
$self->sendCmd($cmd);
}
sub moveStop {
my $self = shift;
my $params = shift;
my $speed = 0;
Debug('Move Stop');
# we have to stop both pans and zooms
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$speed,$speed";
$self->sendCmd($cmd);
my $cmd = "/axis-cgi/com/ptz.cgi?continuouszoommove=$speed";
$self->sendCmd($cmd);
}
sub zoomRelTele {
my $self = shift;
my $params = shift;
@ -425,20 +490,15 @@ __END__
=head1 NAME
ZoneMinder::Database - Perl extension for blah blah blah
ZoneMinder::Control::Axis - Zoneminder control for Axis Cameras using the V2 API
=head1 SYNOPSIS
use ZoneMinder::Database;
blah blah blah
use ZoneMinder::Control::AxisV2 ; place this in /usr/share/perl5/ZoneMinder/Control
=head1 DESCRIPTION
Stub documentation for ZoneMinder, created by h2xs. It looks like the
author of the extension was negligent enough to leave the stub
unedited.
Blah blah blah.
This module is an implementation of the Axis V2 API
=head2 EXPORT
@ -448,14 +508,8 @@ None by default.
=head1 SEE ALSO
Mention other useful documentation such as the documentation of
related modules or operating system documentation (such as man pages
in UNIX), or any relevant external documentation such as RFCs or
standards.
If you have a mailing list set up for your module, mention it here.
If you have a web site set up for your module, mention it here.
AXIS VAPIX Library Documentation; e.g.:
https://www.axis.com/vapix-library/subjects/t10175981/section/t10036011/display
=head1 AUTHOR

View File

@ -0,0 +1,281 @@
# ==========================================================================
#
# ZoneMinder GrandSteam Control Protocol Module
# Copyright (C) 2021 ZoneMinder Inc
#
# 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.
#
# ==========================================================================
#
# This module contains the implementation of the Vivotek ePTZ camera control
# protocol
#
package ZoneMinder::Control::Grandstream;
use 5.006;
use strict;
use warnings;
require ZoneMinder::Base;
require ZoneMinder::Control;
our @ISA = qw(ZoneMinder::Control);
# ==========================================================================
#
# Vivotek ePTZ Control Protocol
#
# ==========================================================================
use ZoneMinder::Logger qw(:all);
use ZoneMinder::Config qw(:all);
use ZoneMinder::General qw(:all);
use Time::HiRes qw( usleep );
use URI::Encode qw(uri_encode);
use XML::LibXML;
use Digest::MD5 qw(md5 md5_hex md5_base64);
our $REALM = '';
our $PROTOCOL = 'https://';
our $USERNAME = 'admin';
our $PASSWORD = '';
our $ADDRESS = '';
our $BASE_URL = '';
my %config_types = (
upgrade => {
P6767 => { default_value=>1, desc=>'Firmware Upgrade Method http' },
P192 => { desc=>'Firmware Server Path' },
},
date => {
P64 => { desc=>'Timezone' },
P5006 => { default_value=>1, desc=>'Enable NTP' },
P30 => { desc=>'NTP Server', },
},
access => {
P12053 => {default_value=>1, desc=>'Enable UPnP Search' },
},
cmos => {
#P12314=> { value=>0, desc=>'Power Frequency' },
},
video => {
#P12306 => { value=>'26', desc=>'primary codec' },# 26: h264, 96: mjpeg, 98: h265
P12313 => { desc=>'primary profile' },# 0: baseline, 1: main, 2: high
P12307 => { desc=>'primary resolution' }, # 1025: 1920x1080 1023: 1280x960, 1022: 1280x720
P12904 => { desc=>'primary fps', }, # fps 5,10,15,20,25,30
P12311 => { desc=>'Image quality', }, # 0 very high, 4 very low
P12312 => { desc=>'Iframe interval', }, # i-frame interval 5-100
},
osd => {
P10044 => { default_value=> 1, desc=>'Display Time' },
#P10045 => { value=> 1, desc=>'Display Text' },
P10001 => { default_value=> 1, desc=>'OSD Date Format' },
#P10040 => { value=>'', desc=> 'OSD Text' },
},
audio => {
P14000 => { default_value=>1, desc=>'Audio codec' }, # 1,2
P14003 => { default_value=>0, desc=>'Audio out volume' }, # 0-6
P14002 => { default_value=>0, desc=>'Audio in volume' }, # 0-6
},
debug => {
P8042 => { default_value=>0, desc=>'Debug log protocol' }, # 0: UDP 1: SSL/TLS
P207 => { desc=>'Debug Log Server' },
P208 => { desc=>'Debug Log Level' },
},
);
sub open {
my $self = shift;
$self->loadMonitor();
if ($self->{Monitor}{ControlAddress}
and
$self->{Monitor}{ControlAddress} ne 'user:pass@ip'
and
$self->{Monitor}{ControlAddress} ne 'user:port@ip'
) {
Debug("Getting connection details from Path " . $self->{Monitor}->{ControlAddress});
if (($self->{Monitor}->{ControlAddress} =~ /^(?<PROTOCOL>https?:\/\/)?(?<USERNAME>[^:@]+)?:?(?<PASSWORD>[^\/@]+)?@?(?<ADDRESS>.*)$/)) {
$PROTOCOL = $+{PROTOCOL} if $+{PROTOCOL};
$USERNAME = $+{USERNAME} if $+{USERNAME};
$PASSWORD = $+{PASSWORD} if $+{PASSWORD};
$ADDRESS = $+{ADDRESS} if $+{ADDRESS};
}
} elsif ($self->{Monitor}->{Path}) {
Debug("Getting connection details from Path " . $self->{Monitor}->{Path});
if (($self->{Monitor}->{Path} =~ /^(?<PROTOCOL>(https?|rtsp):\/\/)?(?<USERNAME>[^:@]+)?:?(?<PASSWORD>[^\/@]+)?@?(?<ADDRESS>[^:\/]+)/)) {
$USERNAME = $+{USERNAME} if $+{USERNAME};
$PASSWORD = $+{PASSWORD} if $+{PASSWORD};
$ADDRESS = $+{ADDRESS} if $+{ADDRESS};
}
Debug("username:$USERNAME password:$PASSWORD address:$ADDRESS");
} else {
Error('Failed to parse auth from address ' . $self->{Monitor}->{ControlAddress});
$ADDRESS = $self->{Monitor}->{ControlAddress};
}
$BASE_URL = $PROTOCOL.$ADDRESS;
use LWP::UserAgent;
$self->{ua} = LWP::UserAgent->new;
$self->{ua}->agent('ZoneMinder Control Agent/'.ZoneMinder::Base::ZM_VERSION);
$self->{ua}->ssl_opts(verify_hostname => 0, SSL_verify_mode => 0x00);
$self->{ua}->cookie_jar( {} );
my $rescode = '';
my $url = $BASE_URL.'/goform/login?cmd=login&type=0&user='.$USERNAME;
my $response = $self->get($url);
if ($response->is_success()) {
my $dom = XML::LibXML->load_xml(string => $response->content);
my $challengeString = $dom->getElementsByTagName('ChallengeCode')->string_value();
Debug('challengstring: '.$challengeString);
my $authcode = md5_hex($challengeString.':GSC36XXlZpRsFzCbM:'.$PASSWORD);
$url .= '&authcode='.$authcode;
$response = $self->get($url);
$dom = XML::LibXML->load_xml(string => $response->content);
$rescode = $dom->getElementsByTagName('ResCode');
} else {
Warning("Falling back to old style");
$PROTOCOL = 'http://';
$BASE_URL = $PROTOCOL.$USERNAME.':'.$PASSWORD.'@'.$ADDRESS;
}
$self->{state} = 'open';
}
sub get {
my $self = shift;
my $url = shift;
Debug("Getting $url");
my $response = $self->{ua}->get($url);
Debug('Response: '. $response->status_line . ' ' . $response->content);
return $response;
}
sub close {
my $self = shift;
$self->{state} = 'closed';
}
sub sendCmd {
my ($self, $cmd, $speedcmd) = @_;
$self->printMsg( $speedcmd, 'Tx' );
$self->printMsg( $cmd, 'Tx' );
my $req = HTTP::Request->new( GET => $BASE_URL."/cgi-bin/camctrl/eCamCtrl.cgi?stream=0&$speedcmd&$cmd");
my $res = $self->{ua}->request($req);
if (!$res->is_success) {
Error('Request failed: '.$res->status_line().' (URI: '.$req->as_string().')');
}
return $res->is_success;
}
sub get_config {
my $self = shift;
my %config;
foreach my $category ( @_ ? @_ : keys %config_types ) {
my $response = $self->get($BASE_URL.'/goform/config?cmd=get&type='.$category);
my $dom = XML::LibXML->load_xml(string => $response->content);
if (!$dom) {
Error("No document from :".$response->content());
return;
}
Debug($dom->toString(1));
$config{$category} = {};
my $Configuration = $dom->getElementsByTagName('Configuration');
my $xml = $Configuration->get_node(0);
if (!$xml) {
Warning("UNable to get Configuration node from ".$response->content());
return \%config;
}
foreach my $node ($xml->childNodes()) {
$config{$category}{$node->nodeName} = {
value=>$node->textContent
};
}
} # end foreach category
return \%config;
} # end sub get_config
sub set_config {
my $self = shift;
my $updates = shift;
my $url = join('&', $BASE_URL.'/goform/config?cmd=set',
map { $_.'='.uri_encode(uri_encode($$updates{$_}{value}, { encode_reserved=>1} )) } keys %$updates );
my $response = $self->get($url);
return 0 if !$response->is_success();
return 0 if ($response->content !~ /Successful/i);
return 1;
}
sub reboot {
my $self = shift;
$self->get($BASE_URL.'/goform/config?cmd=reboot');
}
sub ping {
return -1 if ! $ADDRESS;
require Net::Ping;
my $p = Net::Ping->new();
my $rv = $p->ping($ADDRESS);
$p->close();
return $rv;
}
1;
__END__
=head1 NAME
ZoneMinder::Control::Grandstream - ZoneMinder Perl extension for Grandstream
camera control protocol
=head1 SYNOPSIS
use ZoneMinder::Control::Grandstream;
=head1 DESCRIPTION
This module implements the protocol used in various Grandstream IP cameras.
=head2 EXPORT
None.
=head1 SEE ALSO
I would say, see ZoneMinder::Control documentation. But it is a stub.
=head1 AUTHOR
Isaac Connor E<lt>isaac@zoneminder.comE<gt>
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2021 by ZoneMinder Inc
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.3 or,
at your option, any later version of Perl 5 you may have available.
=cut

View File

@ -25,83 +25,114 @@ our $ADDRESS = '';
use ZoneMinder::Logger qw(:all);
use ZoneMinder::Config qw(:all);
use URI;
use LWP::UserAgent;
sub credentials {
my $self = shift;
($USERNAME, $PASSWORD) = @_;
Debug("Setting credentials to $USERNAME/$PASSWORD");
}
sub open {
my $self = shift;
$self->loadMonitor();
if ( ( $self->{Monitor}->{ControlAddress} =~ /^(?<PROTOCOL>https?:\/\/)?(?<USERNAME>[^:@]+)?:?(?<PASSWORD>[^\/@]+)?@?(?<ADDRESS>.*)$/ ) ) {
if ($self->{Monitor}{ControlAddress}
and
$self->{Monitor}{ControlAddress} ne 'user:pass@ip'
and
$self->{Monitor}{ControlAddress} ne 'user:port@ip'
and
($self->{Monitor}->{ControlAddress} =~ /^(?<PROTOCOL>https?:\/\/)?(?<USERNAME>[^:@]+)?:?(?<PASSWORD>[^\/@]+)?@?(?<ADDRESS>.*)$/)
) {
$PROTOCOL = $+{PROTOCOL} if $+{PROTOCOL};
$USERNAME = $+{USERNAME} if $+{USERNAME};
$PASSWORD = $+{PASSWORD} if $+{PASSWORD};
$ADDRESS = $+{ADDRESS} if $+{ADDRESS};
} elsif ($self->{Monitor}{Path}) {
Debug("Using Path for credentials: $self->{Monitor}{Path}");
my $uri = URI->new($self->{Monitor}{Path});
Debug("Using Path for credentials: $self->{Monitor}{Path}" . $uri->userinfo());
( $USERNAME, $PASSWORD ) = split(/:/, $uri->userinfo()) if $uri->userinfo();
$ADDRESS = $uri->host();
} else {
Error('Failed to parse auth from address ' . $self->{Monitor}->{ControlAddress});
$ADDRESS = $self->{Monitor}->{ControlAddress};
}
if ( !($ADDRESS =~ /:/) ) {
Error('You generally need to also specify the port. I will append :80');
Debug('You generally need to also specify the port. I will append :80');
$ADDRESS .= ':80';
}
use LWP::UserAgent;
$self->{ua} = LWP::UserAgent->new;
$self->{ua}->agent('ZoneMinder Control Agent/'.ZoneMinder::Base::ZM_VERSION);
$self->{state} = 'closed';
# credentials: ("ip:port" (no prefix!), realm (string), username (string), password (string)
Debug ( "sendCmd credentials control address:'".$ADDRESS
Debug("sendCmd credentials control address:'".$ADDRESS
."' realm:'" . $REALM
. "' username:'" . $USERNAME
. "' password:'".$PASSWORD
."'"
);
$self->{ua}->credentials($ADDRESS,$REALM,$USERNAME,$PASSWORD);
# Detect REALM
my $res = $self->{ua}->get($PROTOCOL.$ADDRESS.'/cgi/ptdc.cgi');
if ( $res->is_success ) {
$self->{state} = 'open';
$REALM = $self->detect_realm($PROTOCOL, $ADDRESS, $REALM, $USERNAME, $PASSWORD, '/');
if (defined($REALM)) {
return !undef;
}
if ( $res->status_line() eq '401 Unauthorized' ) {
my $headers = $res->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}");
}
if ( $$headers{'www-authenticate'} ) {
my ( $auth, $tokens ) = $$headers{'www-authenticate'} =~ /^(\w+)\s+(.*)$/;
if ( $tokens =~ /\w+="([^"]+)"/i ) {
if ( $REALM ne $1 ) {
$REALM = $1;
Debug("Changing REALM to $REALM");
$self->{ua}->credentials($ADDRESS,$REALM,$USERNAME,$PASSWORD);
$res = $self->{ua}->get($PROTOCOL.$ADDRESS.'/cgi/ptdc.cgi');
if ( $res->is_success() ) {
$self->{state} = 'open';
return !undef;
}
Error('Authentication still failed after updating REALM' . $res->status_line);
$headers = $res->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}");
} # end foreach
} else {
Error('Authentication failed, not a REALM problem');
}
} else {
Error('Failed to match realm in tokens');
} # end if
} else {
Debug('No headers line');
} # end if headers
} # end if $res->status_line() eq '401 Unauthorized'
return undef;
} # end sub open
sub detect_realm {
my ($self, $protocol, $address, $realm, $username, $password, $url) = @_;
$self->{ua}->credentials($address, $realm, $username, $password);
my $res = $self->{ua}->get($protocol.$address.$url);
if ($res->is_success) {
Debug(1, 'Success opening without realm detection for '.$url);
return $realm;
}
if ($res->status_line() ne '401 Unauthorized') {
return $realm;
}
my $headers = $res->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}");
}
if ($$headers{'www-authenticate'}) {
my ( $auth, $tokens ) = $$headers{'www-authenticate'} =~ /^(\w+)\s+(.*)$/;
if ( $tokens =~ /\w+="([^"]+)"/i ) {
if ($realm ne $1) {
$realm = $1;
Debug("Changing REALM to $realm");
$self->{ua}->credentials($address, $realm, $username, $password);
$res = $self->{ua}->get($protocol.$address.$url);
if ($res->is_success()) {
return $realm;
}
Error('Authentication still failed after updating REALM' . $res->status_line);
$headers = $res->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}");
} # end foreach
} else {
Error('Authentication failed, not a REALM problem');
}
} else {
Error('Failed to match realm in tokens');
} # end if
} else {
Debug('No headers line');
} # end if headers
return undef;
}
sub sendCmd {
# This routine is used for all moving, which are all GET commands...
my $self = shift;
@ -137,17 +168,14 @@ sub sendCmdPost {
my $url = shift;
my $form = shift;
my $result = undef;
if ( $url eq undef ) {
if ($url eq undef) {
Error('url passed to sendCmdPost is undefined.');
return -1;
}
#Debug('sendCmdPost url: ' . $url . ' cmd: ' . $cmd);
Debug('sendCmdPost url: ' . $PROTOCOL.$ADDRESS.$url);
my $res;
$res = $self->{ua}->post(
my $res = $self->{ua}->post(
$PROTOCOL.$ADDRESS.$url,
Referer=>$PROTOCOL.$ADDRESS.$url,
Content=>$form
@ -155,13 +183,19 @@ sub sendCmdPost {
Debug("sendCmdPost credentials control to: $PROTOCOL$ADDRESS$url realm:'" . $REALM . "' username:'" . $USERNAME . "' password:'".$PASSWORD."'");
if ( $res->is_success ) {
Debug($res->content);
return !undef;
if (!$res->is_success) {
Error("sendCmdPost Error check failed: '".$res->status_line()."' cmd:");
my $new_realm = $self->detect_realm($PROTOCOL, $ADDRESS, $REALM, $USERNAME, $PASSWORD, $url);
if (defined($new_realm) and ($new_realm ne $REALM)) {
Debug("Success after re-detecting realm. New realm is $new_realm");
return !undef;
}
Warning('Failed to reboot');
return undef;
}
Error("sendCmdPost Error check failed: '".$res->status_line()."' cmd:");
Debug($res->content);
return $result;
return !undef;
} # end sub sendCmdPost
sub move {
@ -378,6 +412,10 @@ sub reset {
sub reboot {
my $self = shift;
Debug('Camera Reboot');
if (!$$self{open}) {
Warning("Not open. opening. Should call ->open() before calling reboot()");
return if !$self->open();
}
$self->sendCmdPost('/eng/admin/reboot.cgi', { reboot => 'true' });
#$referer = 'http://'.$HI->ip().'/eng/admin/tools_default.cgi';
#$initial_url = $HI->ip().'/eng/admin/tools_default.cgi';

185
scripts/zmalarm-server.py Executable file
View File

@ -0,0 +1,185 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ==========================================================================
#
# ZoneMinder Alarm Server Script for Netsurveillence Software cameras, $Date$, $Revision$
# Copyright (C) 2022
#
# 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.
#
# ==========================================================================
# Adds pyzm support
import pyzm.api as zmapi
import pyzm.ZMLog as zmlog
import pyzm.helpers.utils as utils
import os, sys, struct, json
from time import sleep
#import time
from socket import *
# from datetime import *
# telnet
from telnetlib import Telnet
# multi threading
from threading import Thread
def writezmlog(m,s):
zmlog.init()
zmlog.Info(m+s)
# zmlog.close()
def alarm_thread(m, monid,eventlenght):
import subprocess
print("Monitor " + str(monid)+" entered alarm_thread...")
result = subprocess.run(['zmu','-m', str(monid), '-s'] ,stdout=subprocess.PIPE)
if result.stdout.decode('utf-8') != '3\n':
print('Changing monitor '+ str(monid) + ' status to Alarm...')
m.arm()
sleep(eventlenght)
m.disarm()
else:
print('Monitor '+ str(monid) + ' already in status Alarm...')
print('Finishing thread...')
def event_thread(m_id,eventlenght):
import subprocess
print("Monitor " + str(m_id)+" entered event_thread...")
result = subprocess.run(['zmu','-m', str(m_id), '-x'] ,stdout=subprocess.PIPE)
if result.stdout.decode('utf-8') == '0\n':
print('Firing monitor '+ str(m_id) + ' trigger...')
telbuff = str(m_id) + '|on+'+str(eventlenght)+'|1|Human Motion Detected|'
with Telnet('localhost', 6802) as tn:
tn.write(telbuff.encode('ascii') + alarm_desc.encode('ascii') + b'\n')
tn.read_until(b'off')
else:
print('Monitor '+ str(m_id) + ' already triggered, doing nothing...')
print('Finishing thread...')
def tolog(s):
logfile = open(datetime.now().strftime('%Y_%m_%d_') + log, 'a+')
logfile.write(s)
logfile.close()
def GetIP(s):
return inet_ntoa(struct.pack('<I', int(s, 16)))
# config variables
eventlenght = 60
wrzmlog = 'n'
wrzmevent ='n'
rsealm = 'n'
port = '15002'
if len(sys.argv) > 1:
keys = ["--log=","-l=","--alarm=","-a=","--port=","-p=","--event=","-e="]
for i in range(1,len(sys.argv)):
for key in keys:
if sys.argv[i].find(key) == 0:
if key == "--log=" or key == "-l=":
wrzmlog=sys.argv[i][len(key):]
elif key == "--alarm=" or key == "-a=":
rsealm=sys.argv[i][len(key):]
elif key == "--port=" or key == "-p=":
port=sys.argv[i][len(key):]
elif key == "--event=" or key == "-e=":
wrzmevent=sys.argv[i][len(key):]
break
else:
print('Usage: %s [--port|-p=<value> --log|-l=<y/n> --alarm|-a=<y/n> --event|-e=<y/n>]' % os.path.basename(sys.argv[0]))
sys.exit(1)
print ('Create log entry: ', wrzmlog)
print ('Trigger event: ', wrzmlog)
print ('Raise Alarm: ', rsealm)
server = socket(AF_INET, SOCK_STREAM)
server.bind(('0.0.0.0', int(port)))
# server.settimeout(0.5)
server.listen(1)
log = "AlarmServer.log"
conf = utils.read_config('/etc/zm/secrets.ini')
api_options = {
'apiurl': utils.get(key='ZM_API_PORTAL', section='secrets', conf=conf),
'portalurl':utils.get(key='ZM_PORTAL', section='secrets', conf=conf),
'user': utils.get(key='ZM_USER', section='secrets', conf=conf),
#'disable_ssl_cert_check': True
}
zmapi = zmapi.ZMApi(options=api_options)
# importing the regex to get ip out of path
import re
#define regex pattern for IP addresses
pattern =re.compile('''((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)''')
# store the response of URL
#process monitors create dict of monitors
list_monit = {}
zm_monitors = zmapi.monitors()
for m in zm_monitors.list():
ip_v4=pattern.search(m.get()['Path'])
list_monit[ip_v4.group()]=m.id()
writezmlog('Listening on port: '+port,' AlarmServer.py')
print ('Listening on port: '+port)
#run Alarm Server
while True:
try:
conn, addr = server.accept()
head, version, session, sequence_number, msgid, len_data = struct.unpack(
'BB2xII2xHI', conn.recv(20)
)
sleep(0.1) # Just for recive whole packet
data = conn.recv(len_data)
conn.close()
# make the json a Dictionary
reply = json.loads(data)
# get ip
ip_v4 = GetIP(reply.get('Address'))
# get alarm_event_desc
alarm_desc = reply.get('Event')
# print(datetime.now().strftime('[%Y-%m-%d %H:%M:%S]>>>'))
print ('Ip Address: ',ip_v4)
print ("Alarm Description: ", alarm_desc)
print('<<<')
# tolog(repr(data) + "\r\n")
if alarm_desc == 'HumanDetect':
if wrzmlog == 'y':
writezmlog(alarm_desc+' in monitor ',str(list_monit[ip_v4]))
if rsealm == 'y':
print ("Triggering Alarm...")
mthread = Thread(target=alarm_thread, args=(zm_monitors.find(list_monit[ip_v4]),list_monit[ip_v4],eventlenght))
mthread.start()
elif wrzmevent == 'y':
print ("Triggering Event Rec on zmtrigger...")
mthread = Thread(target=event_thread, args=(list_monit[ip_v4],eventlenght))
mthread.start()
except (KeyboardInterrupt, SystemExit):
break
server.close()
# needs to be closed again... otherwise it will crash on exit.
zmlog.close()
sys.exit(1)

View File

@ -102,7 +102,8 @@ my @daemons = (
'zmtrack.pl',
'zmcontrol.pl',
'zm_rtsp_server',
'zmtelemetry.pl'
'zmtelemetry.pl',
'zmalarm-server.py'
);
if ( $Config{ZM_OPT_USE_EVENTNOTIFICATION} ) {

View File

@ -288,6 +288,30 @@ if ( $command =~ /^(?:start|restart)$/ ) {
if ( $Config{ZM_MIN_RTSP_PORT} ) {
runCommand('zmdc.pl start zm_rtsp_server');
}
# run and pass parameters to AlarmServer.py
if ($Config{ZM_OPT_USE_ALARMSERVER} ) {
my $cmd='zmdc.pl start zmalarm-server.py '. $Config{ZM_OPT_ALS_PORT};
if ($Config{ZM_OPT_ALS_LOGENTRY} ) {
$cmd = $cmd . ' --log=y';
}
else {
$cmd = $cmd . ' --log=n';
}
if ($Config{ZM_OPT_ALS_TRIGGEREVENT} ) {
$cmd = $cmd . ' --event=y';
}
else {
$cmd = $cmd . ' --event=n';
}
if ($Config{ZM_OPT_ALS_ALARM} ) {
$cmd = $cmd . ' --alarm=y';
}
else {
$cmd = $cmd . ' --alarm=n';
}
runCommand($cmd);
}
} else {
$retval = 1;
}

View File

@ -78,7 +78,7 @@ $SIG{TERM} = \&TermHandler;
$SIG{INT} = \&TermHandler;
Info('Watchdog starting, pausing for '.START_DELAY.' seconds');
sleep(START_DELAY);
#sleep(START_DELAY);
my $dbh = zmDbConnect();
@ -127,6 +127,12 @@ while (!$zm_terminate) {
# We can't get the last capture time so can't be sure it's died, it might just be starting up.
my $startup_time = zmGetStartupTime($monitor);
if (($now - $startup_time) > $Config{ZM_WATCH_MAX_DELAY}) {
if ($monitor->ControlId()) {
my $control = $monitor->Control();
if ($control and $control->CanReboot() and $control->open()) {
$control->reboot();
}
}
$log->logPrint(ZoneMinder::Logger::WARNING+$monitor->ImportanceNumber(),
"Restarting capture daemon for $$monitor{Name}, no image since startup. ".
"Startup time was $startup_time - now $now > $Config{ZM_WATCH_MAX_DELAY}"
@ -146,9 +152,9 @@ while (!$zm_terminate) {
my $image_delay = $now - $capture_time;
Debug("Monitor $monitor->{Id} last captured $image_delay seconds ago, max is $max_image_delay");
if ($image_delay > $max_image_delay) {
$log->logPrint(ZoneMinder::Logger::WARNING+$monitor->ImportanceNumber(),
'Restarting capture daemon for '.$monitor->{Name}.
", time since last capture $image_delay seconds ($now-$capture_time)");
$log->logPrint(ZoneMinder::Logger::WARNING+$monitor->ImportanceNumber(),
'Restarting capture daemon for '.$monitor->{Name}.
", time since last capture $image_delay seconds ($now-$capture_time)");
$monitor->control('restart');
next;
}

View File

@ -39,6 +39,7 @@ set(ZM_BIN_SRC_FILES
zm_monitorlink_expression.cpp
#zm_monitorlink_token.cpp
zm_monitorstream.cpp
zm_mqtt.cpp
zm_ffmpeg.cpp
zm_ffmpeg_camera.cpp
zm_ffmpeg_input.cpp

View File

@ -63,7 +63,7 @@ Camera::Camera(
{
linesize = width * colours;
pixels = width * height;
imagesize = height * linesize;
imagesize = static_cast<unsigned long long>(height) * linesize;
Debug(2, "New camera id: %d width: %d line size: %d height: %d colours: %d subpixelorder: %d capture: %d",
monitor->Id(), width, linesize, height, colours, subpixelorder, capture);

View File

@ -227,9 +227,10 @@ zmDbRow::~zmDbRow() {
}
zmDbQueue::zmDbQueue() :
mThread(&zmDbQueue::process, this),
mTerminate(false)
{ }
{
mThread = std::thread(&zmDbQueue::process, this);
}
zmDbQueue::~zmDbQueue() {
stop();

View File

@ -58,6 +58,7 @@ Event::Event(
max_score(-1),
//path(""),
//snapshit_file(),
snapshot_file_written(false),
//alarm_file(""),
videoStore(nullptr),
//video_file(""),
@ -412,10 +413,11 @@ void Event::AddFrame(const std::shared_ptr<ZMPacket>&packet) {
Debug(1, "frames %d, score %d max_score %d", frames, score, max_score);
// If this is the first frame, we should add a thumbnail to the event directory
if ((frames == 1) || (score > max_score)) {
if ((frames == 1) || (score > max_score) || (!snapshot_file_written)) {
write_to_db = true; // web ui might show this as thumbnail, so db needs to know about it.
Debug(1, "Writing snapshot to %s", snapshot_file.c_str());
WriteFrameImage(packet->image, packet->timestamp, snapshot_file.c_str());
snapshot_file_written = true;
} else {
Debug(1, "Not Writing snapshot because frames %d score %d > max %d", frames, score, max_score);
}

View File

@ -88,6 +88,7 @@ class Event {
int max_score;
std::string path;
std::string snapshot_file;
bool snapshot_file_written;
std::string alarm_file;
VideoStore *videoStore;

View File

@ -717,9 +717,8 @@ bool EventStream::sendFrame(Microseconds delta_us) {
} else {
bool send_raw = (type == STREAM_JPEG) && ((scale >= ZM_SCALE_BASE) && (zoom == ZM_SCALE_BASE)) && !filepath.empty();
fprintf(stdout, "--" BOUNDARY "\r\n");
if (send_raw) {
fprintf(stdout, "--" BOUNDARY "\r\n");
if (!send_file(filepath)) {
Error("Can't send %s: %s", filepath.c_str(), strerror(errno));
return false;
@ -774,16 +773,11 @@ bool EventStream::sendFrame(Microseconds delta_us) {
}
Image *send_image = prepareImage(image);
if (temp_img_buffer_size < send_image->Size()) {
Debug(1, "Resizing image buffer from %zu to %u",
temp_img_buffer_size, send_image->Size());
delete[] temp_img_buffer;
temp_img_buffer = new uint8_t[send_image->Size()];
temp_img_buffer_size = send_image->Size();
}
reserveTempImgBuffer(send_image->Size());
int img_buffer_size = 0;
uint8_t *img_buffer = temp_img_buffer;
fprintf(stdout, "--" BOUNDARY "\r\n");
switch ( type ) {
case STREAM_JPEG :
send_image->EncodeJpeg(img_buffer, &img_buffer_size);
@ -842,25 +836,17 @@ void EventStream::runStream() {
SystemTimePoint::duration last_frame_offset = Seconds(0);
SystemTimePoint::duration time_to_event = Seconds(0);
std::thread command_processor;
if (connkey) {
command_processor = std::thread(&EventStream::checkCommandQueue, this);
}
while ( !zm_terminate ) {
now = std::chrono::steady_clock::now();
Microseconds delta = Microseconds(0);
send_frame = false;
if ( connkey ) {
// commands may set send_frame to true
while ( checkCommandQueue() && !zm_terminate ) {
// The idea is to loop here processing all commands before proceeding.
}
// Update modified time of the socket .lock file so that we can tell which ones are stale.
if (now - last_comm_update > Hours(1)) {
touch(sock_path_lock);
last_comm_update = now;
}
}
// Get current frame data
FrameData *frame_data = &event_data->frames[curr_frame_id-1];
@ -1075,7 +1061,14 @@ void EventStream::runStream() {
delete vid_stream;
}
closeComms();
if (connkey) {
if (command_processor.joinable()) {
Debug(1, "command_processor is joinable");
command_processor.join();
} else {
Debug(1, "command_processor is not joinable");
}
}
} // end void EventStream::runStream()
bool EventStream::send_file(const std::string &filepath) {

View File

@ -85,6 +85,8 @@ FfmpegCamera::FfmpegCamera(
const Monitor *monitor,
const std::string &p_path,
const std::string &p_second_path,
const std::string &p_user,
const std::string &p_pass,
const std::string &p_method,
const std::string &p_options,
int p_width,
@ -114,10 +116,13 @@ FfmpegCamera::FfmpegCamera(
),
mPath(p_path),
mSecondPath(p_second_path),
mUser(UriEncode(p_user)),
mPass(UriEncode(p_pass)),
mMethod(p_method),
mOptions(p_options),
hwaccel_name(p_hwaccel_name),
hwaccel_device(p_hwaccel_device)
hwaccel_device(p_hwaccel_device),
frameCount(0)
{
mMaskedPath = remove_authentication(mPath);
mMaskedSecondPath = remove_authentication(mSecondPath);
@ -125,7 +130,6 @@ FfmpegCamera::FfmpegCamera(
FFMPEGInit();
}
frameCount = 0;
mCanCapture = false;
error_count = 0;
use_hwaccel = true;
@ -308,6 +312,12 @@ int FfmpegCamera::OpenFfmpeg() {
mFormatContext->interrupt_callback.opaque = this;
mFormatContext->flags |= AVFMT_FLAG_NOBUFFER | AVFMT_FLAG_FLUSH_PACKETS;
if( mUser.length() > 0 ) {
// build the actual uri string with encoded parameters (from the user and pass fields)
mPath = StringToLower(protocol) + "://" + mUser + ":" + mPass + "@" + mMaskedPath.substr(7, std::string::npos);
Debug(1, "Rebuilt URI with encoded parameters: '%s'", mPath.c_str());
}
ret = avformat_open_input(&mFormatContext, mPath.c_str(), input_format, &opts);
if (ret != 0) {
logPrintf(Logger::ERROR + monitor->Importance(),

View File

@ -38,6 +38,8 @@ class FfmpegCamera : public Camera {
std::string mPath;
std::string mMaskedPath;
std::string mSecondPath;
std::string mUser;
std::string mPass;
std::string mMaskedSecondPath;
std::string mMethod;
std::string mOptions;
@ -71,8 +73,10 @@ class FfmpegCamera : public Camera {
public:
FfmpegCamera(
const Monitor *monitor,
const std::string &path,
const std::string &second_path,
const std::string &p_path,
const std::string &p_second_path,
const std::string &p_user,
const std::string &p_pass,
const std::string &p_method,
const std::string &p_options,
int p_width,

View File

@ -13,7 +13,7 @@ FFmpeg_Input::FFmpeg_Input() {
}
FFmpeg_Input::~FFmpeg_Input() {
if ( input_format_context ) {
if (input_format_context) {
Close();
}
} // end ~FFmpeg_Input()
@ -26,9 +26,9 @@ int FFmpeg_Input::Open(
const AVStream * audio_in_stream,
const AVCodecContext * audio_in_ctx
) {
int max_stream_index = video_stream_id = video_in_stream->index;
int max_stream_index = video_stream_id = video_in_stream->index;
if ( audio_in_stream ) {
if (audio_in_stream) {
max_stream_index = video_in_stream->index > audio_in_stream->index ? video_in_stream->index : audio_in_stream->index;
audio_stream_id = audio_in_stream->index;
}
@ -61,17 +61,17 @@ int FFmpeg_Input::Open(const char *filepath) {
streams = new stream[input_format_context->nb_streams];
Debug(2, "Have %d streams", input_format_context->nb_streams);
for ( unsigned int i = 0; i < input_format_context->nb_streams; i += 1 ) {
if ( is_video_stream(input_format_context->streams[i]) ) {
for (unsigned int i = 0; i < input_format_context->nb_streams; i += 1) {
if (is_video_stream(input_format_context->streams[i])) {
zm_dump_stream_format(input_format_context, i, 0, 0);
if ( video_stream_id == -1 ) {
if (video_stream_id == -1) {
video_stream_id = i;
// if we break, then we won't find the audio stream
} else {
Warning("Have another video stream.");
}
} else if ( is_audio_stream(input_format_context->streams[i]) ) {
if ( audio_stream_id == -1 ) {
} else if (is_audio_stream(input_format_context->streams[i])) {
if (audio_stream_id == -1) {
Debug(2, "Audio stream is %d", i);
audio_stream_id = i;
} else {
@ -82,10 +82,8 @@ int FFmpeg_Input::Open(const char *filepath) {
}
streams[i].frame_count = 0;
streams[i].context = avcodec_alloc_context3(nullptr);
avcodec_parameters_to_context(streams[i].context, input_format_context->streams[i]->codecpar);
if ( !(streams[i].codec = avcodec_find_decoder(streams[i].context->codec_id)) ) {
if (!(streams[i].codec = avcodec_find_decoder(input_format_context->streams[i]->codecpar->codec_id))) {
Error("Could not find input codec");
avformat_close_input(&input_format_context);
return AVERROR_EXIT;
@ -93,8 +91,15 @@ int FFmpeg_Input::Open(const char *filepath) {
Debug(1, "Using codec (%s) for stream %d", streams[i].codec->name, i);
}
Debug(1, "Allocating");
streams[i].context = avcodec_alloc_context3(streams[i].codec);
Debug(1, "Parameters");
avcodec_parameters_to_context(streams[i].context, input_format_context->streams[i]->codecpar);
Debug(1, "Dumping codec");
zm_dump_codec(streams[i].context);
error = avcodec_open2(streams[i].context, streams[i].codec, nullptr);
if ( error < 0 ) {
if (error < 0) {
Error("Could not open input codec (error '%s')",
av_make_error_string(error).c_str());
avcodec_free_context(&streams[i].context);
@ -102,19 +107,25 @@ int FFmpeg_Input::Open(const char *filepath) {
input_format_context = nullptr;
return error;
}
zm_dump_codec(streams[i].context);
if (!(streams[i].context->time_base.num && streams[i].context->time_base.den)) {
Warning("Setting to default time base");
streams[i].context->time_base.num = 1;
streams[i].context->time_base.den = 90000;
}
} // end foreach stream
if ( video_stream_id == -1 )
if (video_stream_id == -1)
Debug(1, "Unable to locate video stream in %s", filepath);
if ( audio_stream_id == -1 )
if (audio_stream_id == -1)
Debug(3, "Unable to locate audio stream in %s", filepath);
return 1;
} // end int FFmpeg_Input::Open( const char * filepath )
int FFmpeg_Input::Close( ) {
if ( streams ) {
for ( unsigned int i = 0; i < input_format_context->nb_streams; i += 1 ) {
if (streams) {
for (unsigned int i = 0; i < input_format_context->nb_streams; i += 1) {
avcodec_close(streams[i].context);
avcodec_free_context(&streams[i].context);
streams[i].context = nullptr;
@ -123,7 +134,7 @@ int FFmpeg_Input::Close( ) {
streams = nullptr;
}
if ( input_format_context ) {
if (input_format_context) {
avformat_close_input(&input_format_context);
input_format_context = nullptr;
}
@ -131,7 +142,7 @@ int FFmpeg_Input::Close( ) {
} // end int FFmpeg_Input::Close()
AVFrame *FFmpeg_Input::get_frame(int stream_id) {
int frameComplete = false;
bool frameComplete = false;
av_packet_ptr packet{av_packet_alloc()};
if (!packet) {
@ -139,9 +150,9 @@ AVFrame *FFmpeg_Input::get_frame(int stream_id) {
return nullptr;
}
while ( !frameComplete ) {
while (!frameComplete) {
int ret = av_read_frame(input_format_context, packet.get());
if ( ret < 0 ) {
if (ret < 0) {
if (
// Check if EOF.
(ret == AVERROR_EOF || (input_format_context->pb && input_format_context->pb->eof_reached)) ||
@ -159,7 +170,7 @@ AVFrame *FFmpeg_Input::get_frame(int stream_id) {
av_packet_guard pkt_guard{packet};
if ( (stream_id >= 0) && (packet->stream_index != stream_id) ) {
if ((stream_id >= 0) && (packet->stream_index != stream_id)) {
Debug(1,"Packet is not for our stream (%d)", packet->stream_index );
continue;
}
@ -178,22 +189,36 @@ AVFrame *FFmpeg_Input::get_frame(int stream_id) {
frame = nullptr;
continue;
} else {
if ( is_video_stream(input_format_context->streams[packet->stream_index]) ) {
if (is_video_stream(input_format_context->streams[packet->stream_index])) {
zm_dump_video_frame(frame.get(), "resulting video frame");
} else {
zm_dump_frame(frame.get(), "resulting frame");
}
}
frameComplete = 1;
frameComplete = true;
if (context->time_base.num && context->time_base.den) {
// Convert timestamps to stream timebase instead of codec timebase
frame->pts = av_rescale_q(frame->pts,
context->time_base,
input_format_context->streams[stream_id]->time_base
);
} else {
Warning("No timebase set in context!");
}
if (is_video_stream(input_format_context->streams[packet->stream_index])) {
zm_dump_video_frame(frame.get(), "resulting video frame");
} else {
zm_dump_frame(frame.get(), "resulting frame");
}
} // end while !frameComplete
if (is_video_stream(input_format_context->streams[packet->stream_index])) {
zm_dump_video_frame(frame.get(), "resulting video frame");
} else {
zm_dump_frame(frame.get(), "resulting frame");
}
return frame.get();
} // end AVFrame *FFmpeg_Input::get_frame
@ -252,7 +277,8 @@ AVFrame *FFmpeg_Input::get_frame(int stream_id, double at) {
last_seek_request = seek_target;
if (frame->pts + frame->pkt_duration < seek_target) {
// Normally it is likely just the next packet. Need a heuristic for seeking, something like duration * keyframe interval
if (frame->pts + 10*frame->pkt_duration < seek_target) {
Debug(1, "Jumping ahead");
if (( ret = av_seek_frame(input_format_context, stream_id, seek_target,
AVSEEK_FLAG_FRAME
@ -264,14 +290,14 @@ AVFrame *FFmpeg_Input::get_frame(int stream_id, double at) {
get_frame(stream_id);
}
// Seeking seems to typically seek to a keyframe, so then we have to decode until we get the frame we want.
if ( frame->pts <= seek_target ) {
if ( is_video_stream(input_format_context->streams[stream_id]) ) {
zm_dump_video_frame(frame, "pts <= seek_target");
} else {
zm_dump_frame(frame, "pts <= seek_target");
}
while ( frame && (frame->pts < seek_target) ) {
if ( !get_frame(stream_id) ) {
if (frame->pts <= seek_target) {
while (frame && (frame->pts + frame->pkt_duration < seek_target)) {
if (is_video_stream(input_format_context->streams[stream_id])) {
zm_dump_video_frame(frame, "pts <= seek_target");
} else {
zm_dump_frame(frame, "pts <= seek_target");
}
if (!get_frame(stream_id)) {
Warning("Got no frame. returning nothing");
return frame.get();
}

View File

@ -81,7 +81,11 @@ void zmFifoDbgOutput(
int len = va_arg(arg_ptr, int);
dbg_ptr += snprintf(dbg_ptr, str_size-(dbg_ptr-dbg_string), "%d:", len);
for ( int i = 0; i < len; i++ ) {
dbg_ptr += snprintf(dbg_ptr, str_size-(dbg_ptr-dbg_string), " %02x", data[i]);
const auto max_len = str_size - (dbg_ptr - dbg_string);
int rc = snprintf(dbg_ptr, max_len, " %02x", data[i]);
if (rc < 0 || rc > max_len)
break;
dbg_ptr += rc;
}
} else {
dbg_ptr += vsnprintf(dbg_ptr, str_size-(dbg_ptr-dbg_string), fstring, arg_ptr);

View File

@ -101,7 +101,7 @@ FontLoadError ZmFont::LoadFontFile(const std::string &loc) {
}
std::vector<uint64> bitmap;
bitmap.resize(bitmap_header.number_of_code_points * bitmap_header.char_height);
bitmap.resize(static_cast<std::size_t>(bitmap_header.number_of_code_points) * bitmap_header.char_height);
std::size_t bitmap_bytes = bitmap.size() * sizeof(uint64);
font_file.read(reinterpret_cast<char *>(bitmap.data()), static_cast<std::streamsize>(bitmap_bytes));

View File

@ -667,7 +667,7 @@ void Image::AssignDirect(
return;
}
size_t new_buffer_size = p_width * p_height * p_colours;
size_t new_buffer_size = static_cast<size_t>(p_width) * p_height * p_colours;
if ( buffer_size < new_buffer_size ) {
Error("Attempt to directly assign buffer from an undersized buffer of size: %zu, needed %dx%d*%d colours = %zu",
@ -2759,7 +2759,7 @@ void Image::Scale(const unsigned int new_width, const unsigned int new_height) {
if (width == new_width and height == new_height) return;
// Why larger than we need?
size_t scale_buffer_size = (new_width+1) * (new_height+1) * colours;
size_t scale_buffer_size = static_cast<size_t>(new_width+1) * (new_height+1) * colours;
uint8_t* scale_buffer = AllocBuffer(scale_buffer_size);
AVPixelFormat format = AVPixFormat();
@ -2789,7 +2789,7 @@ void Image::Scale(const unsigned int factor) {
unsigned int new_height = (height*factor)/ZM_SCALE_BASE;
// Why larger than we need?
size_t scale_buffer_size = (new_width+1) * (new_height+1) * colours;
size_t scale_buffer_size = static_cast<size_t>(new_width+1) * (new_height+1) * colours;
uint8_t* scale_buffer = AllocBuffer(scale_buffer_size);

View File

@ -104,6 +104,8 @@ void LibvlcUnlockBuffer(void* opaque, void* picture, void *const *planes) {
LibvlcCamera::LibvlcCamera(
const Monitor *monitor,
const std::string &p_path,
const std::string &p_user,
const std::string &p_pass,
const std::string &p_method,
const std::string &p_options,
int p_width,
@ -131,6 +133,8 @@ LibvlcCamera::LibvlcCamera(
p_record_audio
),
mPath(p_path),
mUser(UriEncode(p_user)),
mPass(UriEncode(p_pass)),
mMethod(p_method),
mOptions(p_options)
{
@ -214,6 +218,8 @@ int LibvlcCamera::PrimeCapture() {
opVect = Split(Options(), ",");
Debug(1, "Method: '%s'", Method().c_str());
// Set transport method as specified by method field, rtpUni is default
if ( Method() == "rtpMulti" )
opVect.push_back("--rtsp-mcast");
@ -241,6 +247,17 @@ int LibvlcCamera::PrimeCapture() {
}
(*libvlc_log_set_f)(mLibvlcInstance, LibvlcCamera::log_callback, nullptr);
// recreate the path with encoded authentication info
if( mUser.length() > 0 ) {
std::string mMaskedPath = remove_authentication(mPath);
std::string protocol = StringToUpper(mPath.substr(0, 4));
if ( protocol == "RTSP" ) {
// build the actual uri string with encoded parameters (from the user and pass fields)
mPath = StringToLower(protocol) + "://" + mUser + ":" + mPass + "@" + mMaskedPath.substr(7, std::string::npos);
Debug(1, "Rebuilt URI with encoded parameters: '%s'", mPath.c_str());
}
}
mLibvlcMedia = (*libvlc_media_new_location_f)(mLibvlcInstance, mPath.c_str());
if ( mLibvlcMedia == nullptr ) {

View File

@ -49,6 +49,8 @@ class LibvlcCamera : public Camera {
static void log_callback( void *ptr, int level, const libvlc_log_t *ctx, const char *format, va_list vargs );
protected:
std::string mPath;
std::string mUser;
std::string mPass;
std::string mMethod;
std::string mOptions;
StringVector opVect; // mOptArgV will point into opVect so it needs to hang around
@ -62,7 +64,7 @@ protected:
libvlc_media_player_t *mLibvlcMediaPlayer;
public:
LibvlcCamera( const Monitor *monitor, const std::string &path, const std::string &p_method, const std::string &p_options, int p_width, int p_height, int p_colours, int p_brightness, int p_contrast, int p_hue, int p_colour, bool p_capture, bool p_record_audio );
LibvlcCamera( const Monitor *monitor, const std::string &path, const std::string &user,const std::string &pass, const std::string &p_method, const std::string &p_options, int p_width, int p_height, int p_colours, int p_brightness, int p_contrast, int p_hue, int p_colour, bool p_capture, bool p_record_audio );
~LibvlcCamera();
const std::string &Path() const { return mPath; }

View File

@ -387,7 +387,7 @@ void Logger::openFile() {
if (mLogFile.size()) {
if ( (mLogFileFP = fopen(mLogFile.c_str(), "a")) == nullptr ) {
mFileLevel = NOLOG;
Error("fopen() for %s, error = %s", mLogFile.c_str(), strerror(errno));
Error("fopen() for %s %d, error = %s", mLogFile.c_str(), mLogFile.size(), strerror(errno));
}
} else {
puts("Called Logger::openFile() without a filename");
@ -481,7 +481,11 @@ void Logger::logPrint(bool hex, const char *filepath, int line, int level, const
int i;
logPtr += snprintf(logPtr, sizeof(logString)-(logPtr-logString), "%d:", len);
for ( i = 0; i < len; i++ ) {
logPtr += snprintf(logPtr, sizeof(logString)-(logPtr-logString), " %02x", data[i]);
const size_t max_len = sizeof(logString) - (logPtr - logString);
int rc = snprintf(logPtr, max_len, " %02x", data[i]);
if (rc < 0 || static_cast<size_t>(rc) > max_len)
break;
logPtr += rc;
}
} else {
logPtr += vsnprintf(logPtr, sizeof(logString)-(logPtr-logString), fstring, argPtr);

View File

@ -82,7 +82,7 @@ struct Namespace namespaces[] =
std::string load_monitor_sql =
"SELECT `Id`, `Name`, `ServerId`, `StorageId`, `Type`, `Capturing`+0, `Analysing`+0, `AnalysisSource`+0, `AnalysisImage`+0,"
"`Recording`+0, `RecordingSource`+0, `Decoding`+0, "
"`JanusEnabled`, `JanusAudioEnabled`, "
"`JanusEnabled`, `JanusAudioEnabled`, `Janus_Profile_Override`, `Janus_Use_RTSP_Restream`,"
"`LinkedMonitors`, `EventStartCommand`, `EventEndCommand`, `AnalysisFPSLimit`, `AnalysisUpdateDelay`, `MaxFPS`, `AlarmMaxFPS`,"
"`Device`, `Channel`, `Format`, `V4LMultiBuffer`, `V4LCapturesPerFrame`, " // V4L Settings
"`Protocol`, `Method`, `Options`, `User`, `Pass`, `Host`, `Port`, `Path`, `SecondPath`, `Width`, `Height`, `Colours`, `Palette`, `Orientation`+0, `Deinterlacing`, "
@ -96,9 +96,12 @@ std::string load_monitor_sql =
"`SectionLength`, `MinSectionLength`, `FrameSkip`, `MotionFrameSkip`, "
"`FPSReportInterval`, `RefBlendPerc`, `AlarmRefBlendPerc`, `TrackMotion`, `Exif`,"
"`RTSPServer`, `RTSPStreamName`, `ONVIF_Alarm_Text`,"
"`ONVIF_URL`, `ONVIF_Username`, `ONVIF_Password`, `ONVIF_Options`, `ONVIF_Event_Listener`, `use_Amcrest_API`, "
"`SignalCheckPoints`, `SignalCheckColour`, `Importance`-1, ZoneCount FROM `Monitors`";
"`ONVIF_URL`, `ONVIF_Username`, `ONVIF_Password`, `ONVIF_Options`, `ONVIF_Event_Listener`, `use_Amcrest_API`,"
"`SignalCheckPoints`, `SignalCheckColour`, `Importance`-1, ZoneCount "
#if MOSQUITTOPP_FOUND
", `MQTT_Enabled`, `MQTT_Subscriptions`"
#endif
" FROM `Monitors`";
std::string CameraType_Strings[] = {
"Unknown",
@ -166,13 +169,15 @@ Monitor::Monitor()
decoding(DECODING_ALWAYS),
janus_enabled(false),
janus_audio_enabled(false),
janus_profile_override(""),
janus_use_rtsp_restream(false),
//protocol
//method
//options
//host
//port
//user
//pass
user(),
pass(),
//path
//device
palette(0),
@ -277,6 +282,11 @@ Monitor::Monitor()
decoder(nullptr),
convert_context(nullptr),
//zones(nullptr),
#if MOSQUITTOPP_FOUND
mqtt_enabled(false),
// mqtt_subscriptions,
mqtt(nullptr),
#endif
privacy_bitmask(nullptr),
//linked_monitors_string
n_linked_monitors(0),
@ -295,13 +305,13 @@ Monitor::Monitor()
grayscale_val(0),
colour_val(0)
{
if ( strcmp(config.event_close_mode, "time") == 0 )
if (strcmp(config.event_close_mode, "time") == 0) {
event_close_mode = CLOSE_TIME;
else if ( strcmp(config.event_close_mode, "alarm") == 0 )
} else if (strcmp(config.event_close_mode, "alarm") == 0) {
event_close_mode = CLOSE_ALARM;
else
} else {
event_close_mode = CLOSE_IDLE;
}
event = nullptr;
@ -313,8 +323,7 @@ Monitor::Monitor()
/*
std::string load_monitor_sql =
"SELECT `Id`, `Name`, `ServerId`, `StorageId`, `Type`, `Capturing`+0, `Analysing`+0, `AnalysisSource`+0, `AnalysisImage`+0,"
"`Recording`+0, `RecordingSource`+0,
`Decoding`+0, JanusEnabled, JanusAudioEnabled, "
"`Recording`+0, `RecordingSource`+0, `Decoding`+0, JanusEnabled, JanusAudioEnabled, Janus_Profile_Override, Janus_Use_RTSP_Restream"
"LinkedMonitors, `EventStartCommand`, `EventEndCommand`, "
"AnalysisFPSLimit, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS,"
"Device, Channel, Format, V4LMultiBuffer, V4LCapturesPerFrame, " // V4L Settings
@ -328,7 +337,7 @@ Monitor::Monitor()
"FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, Exif,"
"`RTSPServer`,`RTSPStreamName`,
"`ONVIF_URL`, `ONVIF_Username`, `ONVIF_Password`, `ONVIF_Options`, `ONVIF_Event_Listener`, `use_Amcrest_API`, "
"SignalCheckPoints, SignalCheckColour, Importance-1, ZoneCount FROM Monitors";
"SignalCheckPoints, SignalCheckColour, Importance-1, ZoneCount, `MQTT_Enabled`, `MQTT_Subscriptions` FROM Monitors";
*/
void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) {
@ -375,6 +384,8 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) {
// See below after save_jpegs for a recalculation of decoding_enabled
janus_enabled = dbrow[col] ? atoi(dbrow[col]) : false; col++;
janus_audio_enabled = dbrow[col] ? atoi(dbrow[col]) : false; col++;
janus_profile_override = std::string(dbrow[col] ? dbrow[col] : ""); col++;
janus_use_rtsp_restream = dbrow[col] ? atoi(dbrow[col]) : false; col++;
linked_monitors_string = dbrow[col] ? dbrow[col] : ""; col++;
event_start_command = dbrow[col] ? dbrow[col] : ""; col++;
@ -522,6 +533,13 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) {
if (importance < 0) importance = 0; // Should only be >= 0
zone_count = dbrow[col] ? atoi(dbrow[col]) : 0;// col++;
#if MOSQUITTOPP_FOUND
mqtt_enabled = (*dbrow[col] != '0'); col++;
std::string mqtt_subscriptions_string = std::string(dbrow[col] ? dbrow[col] : "");
mqtt_subscriptions = Split(mqtt_subscriptions_string, ','); col++;
Error("MQTT enabled ? %d, subs %s", mqtt_enabled, mqtt_subscriptions_string.c_str());
#endif
// How many frames we need to have before we start analysing
ready_count = std::max(warmup_count, pre_event_count);
@ -619,6 +637,8 @@ void Monitor::LoadCamera() {
host, // Host
port, // Port
path, // Path
user,
pass,
camera_width,
camera_height,
rtsp_describe,
@ -655,6 +675,8 @@ void Monitor::LoadCamera() {
camera = zm::make_unique<FfmpegCamera>(this,
path,
second_path,
user,
pass,
method,
options,
camera_width,
@ -692,6 +714,8 @@ void Monitor::LoadCamera() {
#if HAVE_LIBVLC
camera = zm::make_unique<LibvlcCamera>(this,
path.c_str(),
user,
pass,
method,
options,
camera_width,
@ -1013,6 +1037,15 @@ bool Monitor::connect() {
Janus_Manager = new JanusManager(this);
}
#if MOSQUITTOPP_FOUND
if (mqtt_enabled) {
mqtt = zm::make_unique<MQTT>(this);
for (const std::string &subscription : mqtt_subscriptions) {
if (!subscription.empty())
mqtt->add_subscription(subscription);
}
}
#endif
} else if (!shared_data->valid) {
Error("Shared data not initialised by capture daemon for monitor %s", name.c_str());
return false;
@ -1082,15 +1115,17 @@ bool Monitor::disconnect() {
} // end bool Monitor::disconnect()
Monitor::~Monitor() {
#if MOSQUITTOPP_FOUND
if (mqtt) {
mqtt->send("offline");
}
#endif
Close();
Debug(1, "Done close");
if (mem_ptr != nullptr) {
if (purpose != QUERY) {
Debug(1, "Memsetting");
memset(mem_ptr, 0, mem_size);
} // end if purpose != query
Debug(1, "disconnect");
disconnect();
} // end if mem_ptr
@ -1099,25 +1134,18 @@ Monitor::~Monitor() {
decoder_it = nullptr;
delete storage;
Debug(1, "Done storage");
delete linked_monitors;
linked_monitors = nullptr;
Debug(1, "Don linked monitors");
if (video_fifo) delete video_fifo;
if (audio_fifo) delete audio_fifo;
Debug(1, "Don fifo");
if (convert_context) {
Debug(1, "Don fifo");
sws_freeContext(convert_context);
convert_context = nullptr;
}
Debug(1, "Don fifo");
if (Amcrest_Manager != nullptr) {
Debug(1, "Don fifo");
delete Amcrest_Manager;
} else {
Debug(1, "No amcrest");
}
} // end Monitor::~Monitor()
@ -1668,6 +1696,11 @@ void Monitor::UpdateFPS() {
Info("%s: %d - Capturing at %.2lf fps, capturing bandwidth %ubytes/sec Analysing at %.2lf fps",
name.c_str(), image_count, new_capture_fps, new_capture_bandwidth, new_analysis_fps);
#if MOSQUITTOPP_FOUND
if (mqtt) mqtt->send(stringtf("Capturing at %.2lf fps, capturing bandwidth %ubytes/sec Analysing at %.2lf fps",
new_capture_fps, new_capture_bandwidth, new_analysis_fps));
#endif
shared_data->capture_fps = new_capture_fps;
last_fps_time = now;
last_capture_image_count = image_count;
@ -1916,13 +1949,13 @@ bool Monitor::Analyse() {
/* try to stay behind the decoder. */
if (decoding != DECODING_NONE) {
while (!snap->decoded and !zm_terminate and !analysis_thread->Stopped()) {
while (!snap->decoded and !zm_terminate and !analysis_thread->Stopped() and !packetqueue.stopping()) {
// Need to wait for the decoder thread.
packetqueue.notify_all();
Debug(1, "Waiting for decode");
packet_lock->wait();
} // end while ! decoded
if (zm_terminate or analysis_thread->Stopped()) {
if (zm_terminate or analysis_thread->Stopped() or packetqueue.stopping()) {
delete packet_lock;
return false;
}
@ -2129,11 +2162,12 @@ bool Monitor::Analyse() {
shared_data->state = state = ((shared_data->recording == RECORDING_ALWAYS) ? IDLE : TAPE);
} else {
Debug(1,
"State %d %s because analysis_image_count(%d)-last_alarm_count(%d) > post_event_count(%d) and timestamp.tv_sec(%" PRIi64 ") - recording.tv_src(%" PRIi64 ") >= min_section_length(%" PRIi64 ")",
"State %d %s because analysis_image_count(%d)-last_alarm_count(%d) = %d > post_event_count(%d) and timestamp.tv_sec(%" PRIi64 ") - recording.tv_src(%" PRIi64 ") >= min_section_length(%" PRIi64 ")",
state,
State_Strings[state].c_str(),
analysis_image_count,
last_alarm_count,
analysis_image_count - last_alarm_count,
post_event_count,
static_cast<int64>(std::chrono::duration_cast<Seconds>(snap->timestamp.time_since_epoch()).count()),
static_cast<int64>(std::chrono::duration_cast<Seconds>(GetVideoWriterStartTime().time_since_epoch()).count()),
@ -2152,8 +2186,6 @@ bool Monitor::Analyse() {
Event::AddPreAlarmFrame(snap->image, snap->timestamp, score, nullptr);
} else if (state == ALARM) {
if (event) {
if (noteSetMap.size() > 0)
event->updateNotes(noteSetMap);
if (section_length >= Seconds(min_section_length) && (event->Duration() >= section_length)) {
Warning("%s: %03d - event %" PRIu64 ", has exceeded desired section length. %" PRIi64 " - %" PRIi64 " = %" PRIi64 " >= %" PRIi64,
name.c_str(), analysis_image_count, event->Id(),
@ -2163,6 +2195,8 @@ bool Monitor::Analyse() {
static_cast<int64>(Seconds(section_length).count()));
closeEvent();
event = openEvent(snap, cause, noteSetMap);
} else if (noteSetMap.size() > 0) {
event->updateNotes(noteSetMap);
}
} else if (shared_data->recording != RECORDING_NONE) {
event = openEvent(snap, cause, noteSetMap);
@ -2738,24 +2772,29 @@ void Monitor::TimestampImage(Image *ts_image, SystemTimePoint ts_time) const {
while (*s_ptr && ((unsigned int)(d_ptr - label_text) < (unsigned int) sizeof(label_text))) {
if ( *s_ptr == config.timestamp_code_char[0] ) {
const auto max_len = sizeof(label_text) - (d_ptr - label_text);
bool found_macro = false;
int rc = 0;
switch ( *(s_ptr+1) ) {
case 'N' :
d_ptr += snprintf(d_ptr, sizeof(label_text)-(d_ptr-label_text), "%s", name.c_str());
rc = snprintf(d_ptr, max_len, "%s", name.c_str());
found_macro = true;
break;
case 'Q' :
d_ptr += snprintf(d_ptr, sizeof(label_text)-(d_ptr-label_text), "%s", trigger_data->trigger_showtext);
rc = snprintf(d_ptr, max_len, "%s", trigger_data->trigger_showtext);
found_macro = true;
break;
case 'f' :
typedef std::chrono::duration<int64, std::centi> Centiseconds;
Centiseconds centi_sec = std::chrono::duration_cast<Centiseconds>(
ts_time.time_since_epoch() - std::chrono::duration_cast<Seconds>(ts_time.time_since_epoch()));
d_ptr += snprintf(d_ptr, sizeof(label_text) - (d_ptr - label_text), "%02lld", static_cast<long long int>(centi_sec.count()));
rc = snprintf(d_ptr, max_len, "%02lld", static_cast<long long int>(centi_sec.count()));
found_macro = true;
break;
}
if (rc < 0 || static_cast<size_t>(rc) > max_len)
break;
d_ptr += rc;
if ( found_macro ) {
s_ptr += 2;
continue;
@ -2801,8 +2840,13 @@ Event * Monitor::openEvent(
event = new Event(this, starting_packet->timestamp, cause, noteSetMap);
shared_data->last_event_id = event->Id();
SetVideoWriterStartTime(starting_packet->timestamp);
strncpy(shared_data->alarm_cause, cause.c_str(), sizeof(shared_data->alarm_cause)-1);
#if MOSQUITTOPP_FOUND
if (mqtt) mqtt->send(stringtf("event start: %" PRId64, event->Id()));
#endif
if (!event_start_command.empty()) {
if (fork() == 0) {
execlp(event_start_command.c_str(),
@ -2841,12 +2885,16 @@ void Monitor::closeEvent() {
} else {
Debug(1, "close event thread is not joinable");
}
#if MOSQUITTOPP_FOUND
if (mqtt) mqtt->send(stringtf("event end: %" PRId64, event->Id()));
#endif
Debug(1, "Starting thread to close event");
close_event_thread = std::thread([](Event *e, const std::string &command){
int64_t event_id = e->Id();
int monitor_id = e->MonitorId();
delete e;
if (!command.empty()) {
if (fork() == 0) {
execlp(command.c_str(), command.c_str(),
@ -2856,7 +2904,6 @@ void Monitor::closeEvent() {
Error("Error execing %s", command.c_str());
}
}
}, event, event_end_command);
Debug(1, "Nulling event");
event = nullptr;

View File

@ -28,9 +28,7 @@
#include "zm_event.h"
#include "zm_fifo.h"
#include "zm_image.h"
#if 0
#include "zm_monitorlink_expression.h"
#endif
#include "zm_mqtt.h"
#include "zm_packet.h"
#include "zm_packetqueue.h"
#include "zm_utils.h"
@ -338,6 +336,7 @@ protected:
//helper class for CURL
static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp);
bool Janus_Healthy;
bool Use_RTSP_Restream;
std::string janus_session;
std::string janus_handle;
std::string janus_endpoint;
@ -345,6 +344,7 @@ protected:
std::string rtsp_username;
std::string rtsp_password;
std::string rtsp_path;
std::string profile_override;
public:
explicit JanusManager(Monitor *parent_);
@ -375,6 +375,8 @@ protected:
DecodingOption decoding; // Whether the monitor will decode h264/h265 packets
bool janus_enabled; // Whether we set the h264/h265 stream up on janus
bool janus_audio_enabled; // Whether we tell Janus to try to include audio.
std::string janus_profile_override; // The Profile-ID to force the stream to use.
bool janus_use_rtsp_restream; // Point Janus at the ZM RTSP output, rather than the camera directly.
std::string protocol;
std::string method;
@ -529,6 +531,12 @@ protected:
std::vector<Zone> zones;
#if MOSQUITTOPP_FOUND
bool mqtt_enabled;
std::vector<std::string> mqtt_subscriptions;
std::unique_ptr<MQTT> mqtt;
#endif
const unsigned char *privacy_bitmask;
std::string linked_monitors_string;

View File

@ -18,14 +18,18 @@
//
#include "zm_monitor.h"
#include <regex>
std::string escape_json_string( std::string input );
Monitor::JanusManager::JanusManager(Monitor *parent_) :
parent(parent_),
Janus_Healthy(false)
{
//constructor takes care of init and calls add_to
parent = parent_;
//parent = parent_;
Use_RTSP_Restream = parent->janus_use_rtsp_restream;
profile_override = parent->janus_profile_override;
if ((config.janus_path != nullptr) && (config.janus_path[0] != '\0')) {
janus_endpoint = config.janus_path;
//remove the trailing slash if present
@ -33,21 +37,18 @@ Monitor::JanusManager::JanusManager(Monitor *parent_) :
} else {
janus_endpoint = "127.0.0.1:8088/janus";
}
std::size_t at_pos = parent->path.find("@", 7);
if (at_pos != std::string::npos) {
//If we find an @ symbol, we have a username/password. Otherwise, passwordless login.
std::size_t colon_pos = parent->path.find(":", 7); //Search for the colon, but only after the rtsp:// text.
if (colon_pos == std::string::npos) {
//Looks like an invalid url
throw std::runtime_error("Cannot Parse URL for Janus.");
}
rtsp_username = parent->path.substr(7, colon_pos-7);
rtsp_password = parent->path.substr(colon_pos+1, at_pos - colon_pos - 1);
rtsp_path = "rtsp://";
rtsp_path += parent->path.substr(at_pos + 1);
rtsp_username = "";
rtsp_password = "";
if( parent->user.length() > 0 ) {
rtsp_username = escape_json_string(parent->user);
rtsp_password = escape_json_string(parent->pass);
}
if (Use_RTSP_Restream) {
int restream_port = config.min_rtsp_port;
rtsp_path = "rtsp://127.0.0.1:" + std::to_string(restream_port) + "/" + parent->rtsp_streamname;
} else {
rtsp_username = "";
rtsp_password = "";
rtsp_path = parent->path;
}
}
@ -108,7 +109,7 @@ int Monitor::JanusManager::check_janus() {
curl_easy_cleanup(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));
Warning("Attempted to send %s to %s and got %s", postData.c_str(), endpoint.c_str(), curl_easy_strerror(res));
janus_session = "";
janus_handle = "";
return -1;
@ -151,7 +152,11 @@ int Monitor::JanusManager::add_to_janus() {
postData += "\", \"type\" : \"rtsp\", \"rtsp_quirk\" : true, ";
postData += "\"url\" : \"";
postData += rtsp_path;
if (rtsp_username != "") {
if (profile_override[0] != '\0') {
postData += "\", \"videofmtp\" : \"";
postData += profile_override;
}
if (rtsp_username.length() > 0) {
postData += "\", \"rtsp_user\" : \"";
postData += rtsp_username;
postData += "\", \"rtsp_pwd\" : \"";
@ -279,3 +284,15 @@ int Monitor::JanusManager::get_janus_handle() {
janus_handle = response.substr(pos + 6, 16);
return 1;
} //get_janus_handle
std::string escape_json_string( std::string input ) {
std::string tmp;
tmp = regex_replace(input, std::regex("\n"), "\\n");
tmp = regex_replace(tmp, std::regex("\b"), "\\b");
tmp = regex_replace(tmp, std::regex("\f"), "\\f");
tmp = regex_replace(tmp, std::regex("\r"), "\\r");
tmp = regex_replace(tmp, std::regex("\t"), "\\t");
tmp = regex_replace(tmp, std::regex("\""), "\\\"");
tmp = regex_replace(tmp, std::regex("[\\\\]"), "\\\\");
return tmp;
}

View File

@ -385,13 +385,7 @@ bool MonitorStream::sendFrame(Image *image, SystemTimePoint timestamp) {
/* double pts = */ vid_stream->EncodeFrame(send_image->Buffer(), send_image->Size(), config.mpeg_timed_frames, delta_time.count());
} else {
if (temp_img_buffer_size < send_image->Size()) {
Debug(1, "Resizing image buffer from %zu to %u",
temp_img_buffer_size, send_image->Size());
delete[] temp_img_buffer;
temp_img_buffer = new uint8_t[send_image->Size()];
temp_img_buffer_size = send_image->Size();
}
reserveTempImgBuffer(send_image->Size());
int img_buffer_size = 0;
unsigned char *img_buffer = temp_img_buffer;
@ -429,7 +423,8 @@ bool MonitorStream::sendFrame(Image *image, SystemTimePoint timestamp) {
) {
if (!zm_terminate) {
// If the pipe was closed, we will get signalled SIGPIPE to exit, which will set zm_terminate
Warning("Unable to send stream frame: %s", strerror(errno));
// ICON: zm_terminate might not get set yet. Make it a debug
Debug(1, "Unable to send stream frame: %s", strerror(errno));
}
return false;
}
@ -532,6 +527,12 @@ void MonitorStream::runStream() {
} else {
Debug(2, "Not using playback_buffer");
} // end if connkey && playback_buffer
std::thread command_processor;
if (connkey) {
command_processor = std::thread(&MonitorStream::checkCommandQueue, this);
}
while (!zm_terminate) {
if (feof(stdout)) {
@ -546,23 +547,6 @@ void MonitorStream::runStream() {
monitor->setLastViewed();
bool was_paused = paused;
bool got_command = false; // commands like zoom should output a frame even if paused
if (connkey) {
while (checkCommandQueue() && !zm_terminate) {
Debug(2, "checking command Queue for connkey: %d", connkey);
// Loop in here until all commands are processed.
Debug(2, "Have checking command Queue for connkey: %d", connkey);
got_command = true;
}
if (zm_terminate) break;
// Update modified time of the socket .lock file so that we can tell which ones are stale.
if (now - last_comm_update > Hours(1)) {
touch(sock_path_lock);
last_comm_update = now;
}
} else {
Debug(1, "No connkey");
} // end if connkey
if (!checkInitialised()) {
if (!loadMonitor(monitor_id)) {
if (!sendTextFrame("Not connected")) {
@ -868,7 +852,14 @@ void MonitorStream::runStream() {
if (zm_terminate)
Debug(1, "zm_terminate");
closeComms();
if (connkey) {
if (command_processor.joinable()) {
Debug(1, "command_processor is joinable");
command_processor.join();
} else {
Debug(1, "command_processor is not joinable");
}
}
} // end MonitorStream::runStream
void MonitorStream::SingleImage(int scale) {

121
src/zm_mqtt.cpp Normal file
View File

@ -0,0 +1,121 @@
#ifdef MOSQUITTOPP_FOUND
#include "zm.h"
#include "zm_logger.h"
#include "zm_mqtt.h"
#include "zm_monitor.h"
#include "zm_time.h"
#include <sstream>
#include <string.h>
MQTT::MQTT(Monitor *monitor) :
mosquittopp("ZoneMinder"),
monitor_(monitor),
connected_(false)
{
mosqpp::lib_init();
connect();
}
void MQTT::connect() {
if (config.mqtt_username[0]) {
Debug(1, "MQTT setting username to %s, password to %s", config.mqtt_username, config.mqtt_password);
int rc = mosqpp::mosquittopp::username_pw_set(config.mqtt_username, config.mqtt_password);
if (MOSQ_ERR_SUCCESS != rc) {
Warning("MQTT username pw set returns %d %s", rc, strerror(rc));
}
}
Debug(1, "MQTT connecting to %s:%d", config.mqtt_hostname, config.mqtt_port);
int rc = mosqpp::mosquittopp::connect(config.mqtt_hostname, config.mqtt_port, 60);
if (MOSQ_ERR_SUCCESS != rc) {
if (MOSQ_ERR_INVAL == rc) {
Warning("MQTT reports invalid parameters to connect");
} else {
Warning("MQTT connect returns %d %s", rc, strerror(rc));
}
} else {
Debug(1, "Starting loop");
loop_start();
}
}
void MQTT::autoconfigure() {
}
void MQTT::disconnect() {
}
void MQTT::on_connect(int rc) {
Debug(1, "Connected with rc %d", rc);
if (rc == MOSQ_ERR_SUCCESS) connected_ = true;
}
void MQTT::on_message(const struct mosquitto_message *message) {
Debug(1, "MQTT: Have message %s: %s", message->topic, message->payload);
}
void MQTT::on_subscribe(int mid, int qos_count, const int *granted_qos) {
Debug(1, "MQTT: Subscribed to topic ");
}
void MQTT::on_publish() {
Debug(1, "MQTT: on_publish ");
}
void MQTT::send(const std::string &message) {
if (!connected_) connect();
std::stringstream mqtt_topic;
//mqtt_topic << "/" << config.mqtt_topic_prefix;
mqtt_topic << config.mqtt_topic_prefix;
mqtt_topic << "/monitor/" << monitor_->Id();
const std::string mqtt_topic_string = mqtt_topic.str();
//Debug(1, "DEBUG: MQTT TOPIC: %s : message %s", mqtt_topic_string.c_str(), message.c_str());
//int rc = publish(&mid, mqtt_topic_string.c_str(), message.length(), message.c_str(), 0, true);
int rc = publish(nullptr, mqtt_topic_string.c_str(), message.length(), message.c_str());
if (MOSQ_ERR_SUCCESS != rc) {
Warning("MQTT publish returns %d %s", rc, strerror(rc));
}
}
void MQTT::addSensor(std::string name, std::string type) {
std::map<std::chrono::milliseconds, double> valuesList;
sensorList.insert ( std::pair<std::string,std::map<std::chrono::milliseconds, double>>(name, valuesList));
}
void MQTT::add_subscription(const std::string &name) {
//, std::function <void(int val)> f) {
int mid;
Debug(1, "MQTT add subscription to %s", name.c_str());
subscribe(&mid, name.c_str());
}
void MQTT::addValue(std::string name, double value) {
sensorListIterator = sensorList.find(name);
Debug(1, "found sensor: %s", sensorListIterator->first.c_str());
// if(it == sensorList.end()) {
// clog<<__FUNCTION__<<" Could not find coresponding sensor name"<<endl;
// } else {
//
// }
// valuesList.insert ( std::pair<std::string,double>(name, value));
std::chrono::milliseconds ms = std::chrono::duration_cast< std::chrono::milliseconds >(
std::chrono::high_resolution_clock::now().time_since_epoch()
);
sensorListIterator->second.insert(std::pair<std::chrono::milliseconds, double>(ms, value));
}
void MQTT::listValues(const std::string &sensor_name) {
Debug(1, "%s", sensor_name.c_str());
auto sensorListIterator = sensorList.find(sensor_name);
Debug(1, "found sensor: %s", sensorListIterator->first.c_str());
for (auto inner_iter=sensorListIterator->second.begin(); inner_iter!=sensorListIterator->second.end(); ++inner_iter) {
std::cout << "ts: " << inner_iter->first.count() << ", value:" << inner_iter->second << std::endl;
}
}
MQTT::~MQTT() {
}
#endif

51
src/zm_mqtt.h Normal file
View File

@ -0,0 +1,51 @@
#ifndef ZM_MQTT_H
#define ZM_MQTT_H
#if MOSQUITTOPP_FOUND
#include "mosquittopp.h"
#include <stdint.h>
#include <iostream>
#include <map>
#include <list>
#include <time.h>
#include <chrono>
#include <functional>
class Monitor;
class MQTT : public mosqpp::mosquittopp {
public:
MQTT(Monitor *);
~MQTT();
void autoconfigure();
void connect();
void disconnect();
void send(const std::string &message);
void addSensor(std::string name, std::string type);
void add_subscription(const std::string &name);
void addValue(std::string name, double value);
void listValues(const std::string &sensor_name);
void on_connect(int rc);
void on_message(const struct mosquitto_message *message);
void on_subscribe(int mid, int qos_count, const int *granted_qos);
void on_publish();
enum sensorTypes {
NUMERIC = 0,
DIGITAL
};
private:
std::map<std::string, std::map<std::chrono::milliseconds, double>> sensorList;
std::map<std::string, std::map<std::chrono::milliseconds, double>>::iterator sensorListIterator;
std::map<std::string, int> actuatorList;
Monitor *monitor_;
bool connected_;
};
#endif // MOSQUITTOPP_FOUND
#endif // ZM_MQTT_H

View File

@ -33,7 +33,9 @@ PacketQueue::PacketQueue():
packet_counts(nullptr),
deleting(false),
keep_keyframes(false),
warned_count(0)
warned_count(0),
has_out_of_order_packets_(false),
max_keyframe_interval_(0)
{
}
@ -87,6 +89,24 @@ bool PacketQueue::queuePacket(std::shared_ptr<ZMPacket> add_packet) {
{
std::unique_lock<std::mutex> lck(mutex);
if (deleting or zm_terminate) return false;
if (!has_out_of_order_packets_ and (add_packet->packet->dts != AV_NOPTS_VALUE)) {
auto rit = pktQueue.rbegin();
// Find the previous packet for the stream, and check dts
while (rit != pktQueue.rend()) {
if ((*rit)->packet->stream_index == add_packet->packet->stream_index) {
if ((*rit)->packet->dts >= add_packet->packet->dts) {
Debug(1, "Have out of order packets");
ZM_DUMP_PACKET((*rit)->packet, "queued_packet");
ZM_DUMP_PACKET(add_packet->packet, "add_packet");
has_out_of_order_packets_ = true;
}
break;
}
rit++;
} // end while
}
pktQueue.push_back(add_packet);
for (
auto iterators_it = iterators.begin();
@ -115,8 +135,8 @@ bool PacketQueue::queuePacket(std::shared_ptr<ZMPacket> add_packet) {
warned_count++;
Warning("You have set the max video packets in the queue to %u."
" The queue is full. Either Analysis is not keeping up or"
" your camera's keyframe interval is larger than this setting."
, max_video_packet_count);
" your camera's keyframe interval %d is larger than this setting."
, max_video_packet_count, max_keyframe_interval_);
}
for (
@ -152,6 +172,9 @@ bool PacketQueue::queuePacket(std::shared_ptr<ZMPacket> add_packet) {
}
} // end foreach iterator
zm_packet->decoded = true; // Have to in case analysis is waiting on it
zm_packet->notify_all();
it = pktQueue.erase(it);
packet_counts[zm_packet->packet->stream_index] -= 1;
Debug(1,
@ -226,12 +249,8 @@ void PacketQueue::clearPackets(const std::shared_ptr<ZMPacket> &add_packet) {
// If not doing passthrough, we don't care about starting with a keyframe so logic is simpler
while ((*pktQueue.begin() != add_packet) and (packet_counts[video_stream_id] > pre_event_video_packet_count + tail_count)) {
std::shared_ptr<ZMPacket> zm_packet = *pktQueue.begin();
ZMLockedPacket *lp = new ZMLockedPacket(zm_packet);
if (!lp->trylock()) {
delete lp;
break;
}
delete lp;
ZMLockedPacket lp(zm_packet);
if (!lp.trylock()) break;
if (is_there_an_iterator_pointing_to_packet(zm_packet)) {
Warning("Found iterator at beginning of queue. Some thread isn't keeping up");
@ -262,7 +281,8 @@ void PacketQueue::clearPackets(const std::shared_ptr<ZMPacket> &add_packet) {
return;
}
int keyframe_interval = 1;
int keyframe_interval_count = 1;
ZMLockedPacket *lp = new ZMLockedPacket(zm_packet);
if (!lp->trylock()) {
Debug(4, "Failed getting lock on first packet");
@ -296,12 +316,13 @@ void PacketQueue::clearPackets(const std::shared_ptr<ZMPacket> &add_packet) {
#endif
if (zm_packet->packet->stream_index == video_stream_id) {
keyframe_interval_count++;
if (zm_packet->keyframe) {
Debug(4, "Have a video keyframe so setting next front to it. Keyframe interval so far is %d", keyframe_interval);
keyframe_interval = 1;
Debug(3, "Have a video keyframe so setting next front to it. Keyframe interval so far is %d", keyframe_interval_count);
if (keyframe_interval_count > max_keyframe_interval_)
max_keyframe_interval_ = keyframe_interval_count;
keyframe_interval_count = 1;
next_front = it;
} else {
keyframe_interval++;
}
++video_packets_to_delete;
if (packet_counts[video_stream_id] - video_packets_to_delete <= pre_event_video_packet_count + tail_count) {
@ -313,15 +334,11 @@ void PacketQueue::clearPackets(const std::shared_ptr<ZMPacket> &add_packet) {
++it;
} // end while
if ((keyframe_interval == 1) and max_video_packet_count) {
Warning("Did not find a second keyframe in the packet queue. It may be that"
" the Max Image Buffer setting is lower than the keyframe interval. We"
" need it to be greater than the keyframe interval.");
}
Debug(1, "Resulting it pointing at latest packet? %d, next front points to begin? %d, Keyframe interval %d",
( *it == add_packet ),
( next_front == pktQueue.begin() ),
keyframe_interval
keyframe_interval_count
);
if (next_front != pktQueue.begin()) {
while (pktQueue.begin() != next_front) {
@ -350,11 +367,14 @@ void PacketQueue::clearPackets(const std::shared_ptr<ZMPacket> &add_packet) {
void PacketQueue::stop() {
deleting = true;
condition.notify_all();
for (const auto p : pktQueue) p->notify_all();
for (const auto &p : pktQueue) {
p->notify_all();
}
}
void PacketQueue::clear() {
deleting = true;
// Why are we notifying?
condition.notify_all();
if (!packet_counts) // special case, not initialised
return;

View File

@ -47,6 +47,8 @@ class PacketQueue {
std::mutex mutex;
std::condition_variable condition;
int warned_count;
bool has_out_of_order_packets_;
int max_keyframe_interval_;
public:
PacketQueue();
@ -61,10 +63,13 @@ class PacketQueue {
bool queuePacket(std::shared_ptr<ZMPacket> packet);
void stop();
bool stopping() const { return deleting; };
void clear();
void dumpQueue();
unsigned int size();
unsigned int get_packet_count(int stream_id) const { return packet_counts[stream_id]; };
bool has_out_of_order_packets() const { return has_out_of_order_packets_; };
int get_max_keyframe_interval() const { return max_keyframe_interval_; };
void clearPackets(const std::shared_ptr<ZMPacket> &packet);
int packet_count(int stream_id);

View File

@ -30,6 +30,8 @@ RemoteCameraRtsp::RemoteCameraRtsp(
const std::string &p_host,
const std::string &p_port,
const std::string &p_path,
const std::string &p_user,
const std::string &p_pass,
int p_width,
int p_height,
bool p_rtsp_describe,
@ -47,6 +49,8 @@ RemoteCameraRtsp::RemoteCameraRtsp(
p_brightness, p_contrast, p_hue, p_colour,
p_capture, p_record_audio),
rtsp_describe(p_rtsp_describe),
user(p_user),
pass(p_pass),
frameCount(0)
{
if ( p_method == "rtpUni" )
@ -110,7 +114,7 @@ void RemoteCameraRtsp::Terminate() {
}
int RemoteCameraRtsp::Connect() {
rtspThread = zm::make_unique<RtspThread>(monitor->Id(), method, protocol, host, port, path, auth, rtsp_describe);
rtspThread = zm::make_unique<RtspThread>(monitor->Id(), method, protocol, host, port, path, user, pass, rtsp_describe);
return 0;
}

View File

@ -38,6 +38,9 @@ protected:
int rtcp_sd;
bool rtsp_describe;
const std::string user;
const std::string pass;
Buffer buffer;
Buffer lastSps;
Buffer lastPps;
@ -57,6 +60,8 @@ public:
const std::string &host,
const std::string &port,
const std::string &path,
const std::string &user,
const std::string &pass,
int p_width,
int p_height,
bool p_rtsp_describe,

View File

@ -135,7 +135,8 @@ RtspThread::RtspThread(
const std::string &host,
const std::string &port,
const std::string &path,
const std::string &auth,
const std::string &user,
const std::string &pass,
bool rtsp_describe) :
mId(id),
mMethod(method),
@ -169,12 +170,16 @@ RtspThread::RtspThread(
mHttpSession = stringtf("%d", rand());
mNeedAuth = false;
StringVector parts = Split(auth, ":");
Debug(2, "# of auth parts %zu", parts.size());
if ( parts.size() > 1 )
mAuthenticator = new zm::Authenticator(parts[0], parts[1]);
else
mAuthenticator = new zm::Authenticator(parts[0], "");
if ( user.length() > 0 && pass.length() > 0 ) {
Debug(2, "# of auth parts 2");
mAuthenticator = new zm::Authenticator(user, pass);
} else if( user.length() > 0 ) {
Debug(2, "# of auth parts 1");
mAuthenticator = new zm::Authenticator(user, "");
} else {
Debug(2, "# of auth parts 0");
mAuthenticator = new zm::Authenticator("", "");
}
mThread = std::thread(&RtspThread::Run, this);
}

View File

@ -96,7 +96,9 @@ private:
void Run();
public:
RtspThread( int id, RtspMethod method, const std::string &protocol, const std::string &host, const std::string &port, const std::string &path, const std::string &auth, bool rtsp_describe );
RtspThread( int id, RtspMethod method, const std::string &protocol, const std::string &host,
const std::string &port, const std::string &path, const std::string &user, const std::string &pass,
bool rtsp_describe );
~RtspThread();
public:

View File

@ -21,6 +21,8 @@
#include "zm_box.h"
#include "zm_monitor.h"
#include "zm_signal.h"
#include <cmath>
#include <sys/file.h>
#include <sys/socket.h>
@ -32,7 +34,7 @@ constexpr Milliseconds StreamBase::MAX_SLEEP;
StreamBase::~StreamBase() {
delete vid_stream;
delete temp_img_buffer;
delete[] temp_img_buffer;
closeComms();
}
@ -108,34 +110,35 @@ void StreamBase::updateFrameRate(double fps) {
}
} // void StreamBase::updateFrameRate(double fps)
bool StreamBase::checkCommandQueue() {
if ( sd >= 0 ) {
CmdMsg msg;
memset(&msg, 0, sizeof(msg));
int nbytes = recvfrom(sd, &msg, sizeof(msg), MSG_DONTWAIT, 0, 0);
if ( nbytes < 0 ) {
if ( errno != EAGAIN ) {
Error("recvfrom(), errno = %d, error = %s", errno, strerror(errno));
return false;
void StreamBase::checkCommandQueue() {
while (!zm_terminate) {
// Update modified time of the socket .lock file so that we can tell which ones are stale.
if (now - last_comm_update > Hours(1)) {
touch(sock_path_lock);
last_comm_update = now;
}
if (sd >= 0) {
CmdMsg msg;
memset(&msg, 0, sizeof(msg));
int nbytes = recvfrom(sd, &msg, sizeof(msg), 0, /*MSG_DONTWAIT*/ 0, 0);
if (nbytes < 0) {
if (errno != EAGAIN) {
Error("recvfrom(), errno = %d, error = %s", errno, strerror(errno));
}
} else {
Debug(2, "Message length is (%d)", nbytes);
processCommand(&msg);
got_command = true;
}
} else if (connkey) {
Warning("No sd in checkCommandQueue, comms not open for connkey %06d?", connkey);
} else {
// Perfectly valid if only getting a snapshot
Debug(1, "No sd in checkCommandQueue, comms not open.");
}
//else if ( (nbytes != sizeof(msg)) )
//{
//Error( "Partial message received, expected %d bytes, got %d", sizeof(msg), nbytes );
//}
else {
Debug(2, "Message length is (%d)", nbytes);
processCommand(&msg);
return true;
}
} else if ( connkey ) {
Warning("No sd in checkCommandQueue, comms not open for connkey %06d?", connkey);
} else {
// Perfectly valid if only getting a snapshot
Debug(1, "No sd in checkCommandQueue, comms not open.");
}
return false;
} // end bool StreamBase::checkCommandQueue()
} // end while !zm_terminate
} // end void StreamBase::checkCommandQueue()
Image *StreamBase::prepareImage(Image *image) {
/* zooming should happen before scaling to preserve quality
@ -400,3 +403,13 @@ void StreamBase::closeComms() {
}
}
} // end void StreamBase::closeComms
void StreamBase::reserveTempImgBuffer(size_t size)
{
if (temp_img_buffer_size < size) {
Debug(1, "Resizing image buffer from %zu to %zu", temp_img_buffer_size, size);
delete[] temp_img_buffer;
temp_img_buffer = new uint8_t[size];
temp_img_buffer_size = size;
}
}

View File

@ -146,8 +146,9 @@ protected:
VideoStream *vid_stream;
CmdMsg msg;
bool got_command = false; // commands like zoom should output a frame even if paused
unsigned char *temp_img_buffer; // Used when encoding or sending file data
uint8_t *temp_img_buffer; // Used when encoding or sending file data
size_t temp_img_buffer_size;
protected:
@ -155,8 +156,9 @@ protected:
bool checkInitialised();
void updateFrameRate(double fps);
Image *prepareImage(Image *image);
bool checkCommandQueue();
void checkCommandQueue();
virtual void processCommand(const CmdMsg *msg)=0;
void reserveTempImgBuffer(size_t size);
public:
StreamBase():
@ -189,6 +191,7 @@ public:
frame_count(0),
last_frame_count(0),
frame_mod(1),
got_command(false),
temp_img_buffer(nullptr),
temp_img_buffer_size(0)
{

View File

@ -379,6 +379,26 @@ std::string UriDecode(const std::string &encoded) {
return retbuf;
}
std::string UriEncode(const std::string &value) {
const char *src = value.c_str();
std::string retbuf;
retbuf.reserve(value.length() * 3); // at most all characters get replaced with the escape
char tmp[5] = "";
while(*src) {
if ( *src == ' ' ) {
retbuf.append("%%20");
} else if ( !( (*src >= 'a' && *src <= 'z') || (*src >= 'A' && *src <= 'Z') ) ) {
sprintf(tmp, "%%%02X", *src);
retbuf.append(tmp);
} else {
retbuf.push_back(*src);
}
src++;
}
return retbuf;
}
QueryString::QueryString(std::istream &input) {
while (!input.eof() && input.peek() > 0) {
//Should eat "param1="

View File

@ -50,6 +50,10 @@ inline std::string StringToUpper(std::string str) {
std::transform(str.begin(), str.end(), str.begin(), ::toupper);
return str;
}
inline std::string StringToLower(std::string str) {
std::transform(str.begin(), str.end(), str.begin(), ::tolower);
return str;
}
StringVector Split(const std::string &str, char delim);
StringVector Split(const std::string &str, const std::string &delim, size_t limit = 0);
@ -130,6 +134,7 @@ constexpr std::size_t size(const T(&)[N]) noexcept { return N; }
std::string mask_authentication(const std::string &url);
std::string remove_authentication(const std::string &url);
std::string UriEncode(const std::string &value);
std::string UriDecode(const std::string &encoded);
class QueryParameter {

View File

@ -53,6 +53,7 @@ VideoStore::CodecData VideoStore::codec_data[] = {
{ AV_CODEC_ID_MJPEG, "mjpeg", "mjpeg", AV_PIX_FMT_YUVJ422P, AV_PIX_FMT_YUVJ422P, AV_HWDEVICE_TYPE_NONE },
{ AV_CODEC_ID_VP9, "vp9", "libvpx-vp9", AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P, AV_HWDEVICE_TYPE_NONE },
{ AV_CODEC_ID_AV1, "av1", "libsvtav1", AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P, AV_HWDEVICE_TYPE_NONE },
{ AV_CODEC_ID_AV1, "av1", "libaom-av1", AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P, AV_HWDEVICE_TYPE_NONE },
#else
{ AV_CODEC_ID_H265, "h265", "libx265", AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P },
@ -154,7 +155,10 @@ bool VideoStore::open() {
oc->metadata = pmetadata;
// Dirty hack to allow us to set flags. Needed for ffmpeg5
out_format = const_cast<AVOutputFormat *>(oc->oformat);
// ffmpeg 5 crashes if we do this
#if !LIBAVFORMAT_VERSION_CHECK(59, 16,100, 9, 0)
out_format->flags |= AVFMT_TS_NONSTRICT; // allow non increasing dts
#endif
const AVCodec *video_out_codec = nullptr;

View File

@ -81,7 +81,7 @@ Options for use with monitors:
-U, --username <username> - When running in authenticated mode the username and
-P, --password <password> - password combination of the given user
-A, --auth <authentication> - Pass authentication hash string instead of user details
-x, --xtrigger - Output the current monitor trigger state, 0 = not triggered, 1 = triggered
=cut
*/
@ -137,7 +137,9 @@ void Usage(int status=-1) {
" -U, --username <username> : When running in authenticated mode the username and\n"
" -P, --password <password> : password combination of the given user\n"
" -A, --auth <authentication> : Pass authentication hash string instead of user details\n"
" -T, --token <token> : Pass JWT token string instead of user details\n"
" -T, --token <token> : Pass JWT token string instead of user details\n"
" -x, --xtrigger : Output the current monitor trigger state, 0 = not triggered, 1 = triggered\n"
"", stderr );
exit(status);
@ -167,11 +169,12 @@ typedef enum {
ZMU_SUSPEND = 0x00400000,
ZMU_RESUME = 0x00800000,
ZMU_LIST = 0x10000000,
ZMU_TRIGGER = 0x20000000,
} Function;
bool ValidateAccess(User *user, int mon_id, int function) {
bool allowed = true;
if ( function & (ZMU_STATE|ZMU_IMAGE|ZMU_TIME|ZMU_READ_IDX|ZMU_WRITE_IDX|ZMU_FPS) ) {
if ( function & (ZMU_STATE|ZMU_IMAGE|ZMU_TIME|ZMU_READ_IDX|ZMU_WRITE_IDX|ZMU_FPS|ZMU_TRIGGER) ) {
if ( user->getStream() < User::PERM_VIEW )
allowed = false;
}
@ -246,6 +249,7 @@ int main(int argc, char *argv[]) {
{"version", 1, nullptr, 'V'},
{"help", 0, nullptr, 'h'},
{"list", 0, nullptr, 'l'},
{"xtrigger", 0, nullptr, 'x'},
{nullptr, 0, nullptr, 0}
};
@ -278,7 +282,7 @@ int main(int argc, char *argv[]) {
while (1) {
int option_index = 0;
int c = getopt_long(argc, argv, "d:m:vsEDLurwei::S:t::fz::ancqhlB::C::H::O::RWU:P:A:V:T:", long_options, &option_index);
int c = getopt_long(argc, argv, "d:m:vsEDLurweix::S:t::fz::ancqhlB::C::H::O::RWU:P:A:V:T:", long_options, &option_index);
if (c == -1) {
break;
}
@ -297,6 +301,9 @@ int main(int argc, char *argv[]) {
case 's':
function |= ZMU_STATE;
break;
case 'x':
function |= ZMU_TRIGGER;
break;
case 'i':
function |= ZMU_IMAGE;
if (optarg)
@ -523,6 +530,15 @@ int main(int argc, char *argv[]) {
have_output = true;
}
}
if ( function & ZMU_TRIGGER ) {
int trgstate = monitor->GetTriggerState();
if ( verbose ) {
printf("Current Triggered state: %s\n", trgstate==0?"Not Triggered":(trgstate==1?"Triggered":"NA"));
} else {
printf("%d", trgstate);
have_output = true;
}
}
if ( function & ZMU_TIME ) {
SystemTimePoint timestamp = monitor->GetTimestamp(image_idx);
if (verbose) {

View File

@ -1 +1 @@
1.37.19
1.37.21

View File

@ -6,39 +6,33 @@ $message = '';
// INITIALIZE AND CHECK SANITY
//
if ( !canView('System') )
$message = 'Insufficient permissions to view log entries for user '.$user['Username'];
// task must be set
if ( !isset($_REQUEST['task']) ) {
if (!isset($_REQUEST['task'])) {
$message = 'This request requires a task to be set';
} else if ( $_REQUEST['task'] != 'query' && $_REQUEST['task'] != 'create' ) {
} else if ($_REQUEST['task'] == 'query') {
if (!canView('System')) {
$message = 'Insufficient permissions to view log entries for user '.$user['Username'];
} else {
$data = queryRequest();
}
} else if ($_REQUEST['task'] == 'create' ) {
global $user;
if (!$user) {
// We allow any logged in user to create logs. This opens us up to DOS by malicious user
$message = 'Insufficient permissions to view log entries for user '.$user['Username'];
} else {
createRequest();
}
} else {
// Only the query and create tasks are supported at the moment
$message = 'Unrecognised task '.$_REQUEST['task'];
} else {
$task = $_REQUEST['task'];
}
if ( $message ) {
if ($message) {
ajaxError($message);
return;
}
//
// MAIN LOOP
//
switch ( $task ) {
case 'create' :
createRequest();
break;
case 'query' :
$data = queryRequest();
break;
default :
ZM\Fatal('Unrecognised task '.$task);
} // end switch task
ajaxResponse($data);
//
@ -46,35 +40,29 @@ ajaxResponse($data);
//
function createRequest() {
if ( !empty($_POST['level']) && !empty($_POST['message']) ) {
if (!empty($_POST['level']) && !empty($_POST['message'])) {
ZM\logInit(array('id'=>'web_js'));
$string = $_POST['message'];
$file = !empty($_POST['file']) ? preg_replace('/\w+:\/\/[\w.:]+\//', '', $_POST['file']) : '';
if ( !empty($_POST['line']) ) {
$line = validInt($_POST['line']);
} else {
$line = NULL;
}
$line = empty($_POST['line']) ? NULL : validInt($_POST['line']);
$levels = array_flip(ZM\Logger::$codes);
if ( !isset($levels[$_POST['level']]) ) {
ZM\Panic('Unexpected logger level '.$_POST['level']);
if (!isset($levels[$_POST['level']])) {
ZM\Error('Unexpected logger level '.$_POST['level']);
$_POST['level'] = 'ERR';
}
$level = $levels[$_POST['level']];
ZM\Logger::fetch()->logPrint($level, $string, $file, $line);
ZM\Logger::fetch()->logPrint($level, $_POST['message'], $file, $line);
} else {
ZM\Error('Invalid log create: '.print_r($_POST, true));
}
}
function queryRequest() {
// Offset specifies the starting row to return, used for pagination
$offset = 0;
if ( isset($_REQUEST['offset']) ) {
if ( ( !is_int($_REQUEST['offset']) and !ctype_digit($_REQUEST['offset']) ) ) {
if (isset($_REQUEST['offset'])) {
if ((!is_int($_REQUEST['offset']) and !ctype_digit($_REQUEST['offset']))) {
ZM\Error('Invalid value for offset: ' . $_REQUEST['offset']);
} else {
$offset = $_REQUEST['offset'];
@ -83,8 +71,8 @@ function queryRequest() {
// Limit specifies the number of rows to return
$limit = 100;
if ( isset($_REQUEST['limit']) ) {
if ( ( !is_int($_REQUEST['limit']) and !ctype_digit($_REQUEST['limit']) ) ) {
if (isset($_REQUEST['limit'])) {
if ((!is_int($_REQUEST['limit']) and !ctype_digit($_REQUEST['limit']))) {
ZM\Error('Invalid value for limit: ' . $_REQUEST['limit']);
} else {
$limit = $_REQUEST['limit'];
@ -100,11 +88,11 @@ function queryRequest() {
$col_alt = array('DateTime', 'Server');
$sort = 'TimeKey';
if ( isset($_REQUEST['sort']) ) {
if (isset($_REQUEST['sort'])) {
$sort = $_REQUEST['sort'];
if ( $sort == 'DateTime' ) $sort = 'TimeKey';
if ($sort == 'DateTime') $sort = 'TimeKey';
}
if ( !in_array($sort, array_merge($columns, $col_alt)) ) {
if (!in_array($sort, array_merge($columns, $col_alt))) {
ZM\Error('Invalid sort field: ' . $sort);
return;
}
@ -127,10 +115,9 @@ function queryRequest() {
$advsearch = isset($_REQUEST['filter']) ? json_decode($_REQUEST['filter'], JSON_OBJECT_AS_ARRAY) : array();
// Search contains a user entered string to search on
$search = isset($_REQUEST['search']) ? $_REQUEST['search'] : '';
if ( count($advsearch) ) {
foreach ( $advsearch as $col=>$text ) {
if ( !in_array($col, array_merge($columns, $col_alt)) ) {
if (count($advsearch)) {
foreach ($advsearch as $col=>$text) {
if (!in_array($col, array_merge($columns, $col_alt))) {
ZM\Error("'$col' is not a searchable column name");
continue;
}
@ -142,8 +129,7 @@ function queryRequest() {
$wherevalues = $query['values'];
$where = ' WHERE (' .implode(' OR ', $likes). ')';
} else if ( $search != '' ) {
} else if ($search != '') {
$search = '%' .$search. '%';
foreach ( $columns as $col ) {
array_push($likes, $col.' LIKE ?');
@ -167,8 +153,8 @@ function queryRequest() {
$results = dbFetchAll($query['sql'], NULL, $query['values']);
global $dateTimeFormatter;
foreach ( $results as $row ) {
$row['DateTime'] = $dateTimeFormatter->format($row['TimeKey']);
foreach ($results as $row) {
$row['DateTime'] = empty($row['TimeKey']) ? '' : $dateTimeFormatter->format(intval($row['TimeKey']));
$Server = ZM\Server::find_one(array('Id'=>$row['ServerId']));
$row['Server'] = $Server ? $Server->Name() : '';

View File

@ -19,14 +19,14 @@ $semaphore_tries = 10;
$have_semaphore = false;
while ($semaphore_tries) {
if ( version_compare( phpversion(), '5.6.1', '<') ) {
if (version_compare( phpversion(), '5.6.1', '<')) {
# don't have support for non-blocking
$have_semaphore = sem_acquire($semaphore);
} else {
$have_semaphore = sem_acquire($semaphore, 1);
}
if ($have_semaphore !== false) break;
ZM\Debug("Failed to get semaphore, trying again");
ZM\Debug('Failed to get semaphore, trying again');
usleep(100000);
$semaphore_tries -= 1;
}
@ -38,7 +38,7 @@ if ($have_semaphore !== false) {
$localSocketFile = ZM_PATH_SOCKS.'/zms-'.sprintf('%06d',$_REQUEST['connkey']).'w.sock';
if ( file_exists($localSocketFile) ) {
ZM\Warning("sock file $localSocketFile already exists?! Is someone else talking to zms?");
// They could be. We can maybe have concurrent requests from a browser.
// They could be. We can maybe have concurrent requests from a browser.
}
if ( !socket_bind($socket, $localSocketFile) ) {
ajaxError("socket_bind( $localSocketFile ) failed: ".socket_strerror(socket_last_error()));
@ -85,8 +85,8 @@ if ($have_semaphore !== false) {
$max_socket_tries = 1000;
// FIXME This should not exceed web_ajax_timeout
while ( !file_exists($remSockFile) && $max_socket_tries-- ) {
//sometimes we are too fast for our own good, if it hasn't been setup yet give it a second.
// WHY? We will just send another one...
//sometimes we are too fast for our own good, if it hasn't been setup yet give it a second.
// WHY? We will just send another one...
// ANSWER: Because otherwise we get a log of errors logged
//ZM\Debug("$remSockFile does not exist, waiting, current " . (time() - $start_time) . ' seconds' );
@ -114,7 +114,7 @@ if ($have_semaphore !== false) {
} else if ( $numSockets < 0 ) {
ajaxError("Socket closed $remSockFile");
} else if ( $numSockets == 0 ) {
ZM\Error("Timed out waiting for msg $remSockFile");
ZM\Error("Timed out waiting for msg $remSockFile after waiting $timeout seconds");
socket_set_nonblock($socket);
#ajaxError("Timed out waiting for msg $remSockFile");
} else if ( $numSockets > 0 ) {
@ -123,7 +123,7 @@ if ($have_semaphore !== false) {
}
}
switch( $nbytes = @socket_recvfrom($socket, $msg, MSG_DATA_SIZE, 0, $remSockFile) ) {
switch ($nbytes = @socket_recvfrom($socket, $msg, MSG_DATA_SIZE, 0, $remSockFile)) {
case -1 :
ajaxError("socket_recvfrom( $remSockFile ) failed: ".socket_strerror(socket_last_error()));
break;
@ -146,14 +146,17 @@ if ($have_semaphore !== false) {
$data['rate'] /= RATE_BASE;
$data['delay'] = round( $data['delay'], 2 );
$data['zoom'] = round( $data['zoom']/SCALE_BASE, 1 );
if (ZM_OPT_USE_AUTH && (ZM_AUTH_RELAY == 'hashed')) {
$auth_hash = generateAuthHash(ZM_AUTH_HASH_IPS);
if (isset($_REQUEST['auth']) and ($_REQUEST['auth'] != $auth_hash)) {
$data['auth'] = $auth_hash;
ZM\Debug('including new auth hash '.$data['auth'].'because doesnt match request auth hash '.$_REQUEST['auth']);
} else {
ZM\Debug('Not including new auth hash becase it hasn\'t changed '.$auth_hash);
}
if (ZM_OPT_USE_AUTH) {
if (ZM_AUTH_RELAY == 'hashed') {
$auth_hash = generateAuthHash(ZM_AUTH_HASH_IPS);
if (isset($_REQUEST['auth']) and ($_REQUEST['auth'] != $auth_hash)) {
$data['auth'] = $auth_hash;
ZM\Debug('including new auth hash '.$data['auth'].'because doesnt match request auth hash '.$_REQUEST['auth']);
} else {
ZM\Debug('Not including new auth hash becase it hasn\'t changed '.$auth_hash);
}
}
$data['auth_relay'] = get_auth_relay();
}
ajaxResponse(array('status'=>$data));
break;
@ -167,11 +170,14 @@ if ($have_semaphore !== false) {
}
$data['rate'] /= RATE_BASE;
$data['zoom'] = round($data['zoom']/SCALE_BASE, 1);
if ( ZM_OPT_USE_AUTH && (ZM_AUTH_RELAY == 'hashed') ) {
$auth_hash = generateAuthHash(ZM_AUTH_HASH_IPS);
if ( isset($_REQUEST['auth']) and ($_REQUEST['auth'] != $auth_hash) ) {
$data['auth'] = $auth_hash;
}
if ( ZM_OPT_USE_AUTH ) {
if (ZM_AUTH_RELAY == 'hashed') {
$auth_hash = generateAuthHash(ZM_AUTH_HASH_IPS);
if ( isset($_REQUEST['auth']) and ($_REQUEST['auth'] != $auth_hash) ) {
$data['auth'] = $auth_hash;
}
}
$data['auth_relay'] = get_auth_relay();
}
ajaxResponse(array('status'=>$data));
break;
@ -180,7 +186,7 @@ if ($have_semaphore !== false) {
}
sem_release($semaphore);
} else {
ajaxError("Unable to get semaphore.");
ajaxError('Unable to get semaphore.');
}
ajaxError('Unrecognised action or insufficient permissions in ajax/stream');

View File

@ -65,7 +65,7 @@ foreach ( $rows as $row ) {
$row['imgHtml'] = '<img id="thumbnail' .$event->Id(). '" src="' .$imgSrc. '" alt="Event '.$event->Id().'" width="' .validInt($event->ThumbnailWidth()). '" height="' .validInt($event->ThumbnailHeight()).'" stream_src="' .$streamSrc. '" still_src="' .$imgSrc. '" loading="lazy" />';
$row['Name'] = validHtmlStr($row['Name']);
$row['StartDateTime'] = $dateTimeFormatter->format(strtotime($row['StartDateTime']));
$row['Length'] = gmdate('H:i:s', $row['Length'] );
$row['Length'] = gmdate('H:i:s', intval($row['Length']));
$returned_rows[] = $row;
} # end foreach row matching search

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -146,6 +146,8 @@ public static function getStatuses() {
'Decoding' => 'Always',
'JanusEnabled' => array('type'=>'boolean','default'=>0),
'JanusAudioEnabled' => array('type'=>'boolean','default'=>0),
'Janus_Profile_Override' => '',
'Janus_Use_RTSP_Restream' => array('type'=>'boolean','default'=>0),
'LinkedMonitors' => array('type'=>'set', 'default'=>null),
'Triggers' => array('type'=>'set','default'=>''),
'EventStartCommand' => '',
@ -243,6 +245,8 @@ public static function getStatuses() {
'RTSPServer' => array('type'=>'boolean', 'default'=>0),
'RTSPStreamName' => '',
'Importance' => 'Normal',
'MQTT_Enabled' => false,
'MQTT_Subscriptions' => '',
);
private $status_fields = array(
'Status' => null,
@ -287,6 +291,74 @@ public static function getStatuses() {
return $this->{'Server'};
}
public function Path($new=null) {
// set the new value if requested
if ($new !== null) {
$this->{'Path'} = $new;
}
// empty value or old auth values terminate
if (!isset($this->{'Path'}) or ($this->{'Path'}==''))
return $this->{'Path'};
// extract the authentication part from the path given
$values = extract_auth_values_from_url($this->{'Path'});
// If no values for User and Pass fields are present then terminate
if (count($values) !== 2) {
return $this->{'Path'};
}
$old_us = isset($this->{'User'}) ? $this->{'User'} : '';
$old_ps = isset($this->{'Pass'}) ? $this->{'Pass'} : '';
$us = $values[0];
$ps = $values[1];
// Update the auth fields if they were empty and remove them from the path
// or if they are equal between the path and field
if ( (!$old_us && !$old_ps) || ($us == $old_us && $ps == $old_ps) ) {
$this->{'Path'} = str_replace("$us:$ps@", '', $this->{'Path'});
$this->{'User'} = $us;
$this->{'Pass'} = $ps;
}
return $this->{'Path'};
}
public function User($new=null) {
if( $new !== null ) {
// no url check if the update has different value
$this->{'User'} = $new;
}
if( strlen($this->{'User'}) > 0 )
return $this->{'User'};
// Only try to update from path if the field is empty
$values = extract_auth_values_from_url($this->{'Path'});
if( count( $values ) == 2 ) {
$us = $values[0];
$this->{'User'} = $values[0];
}
return $this->{'User'};
}
public function Pass($new=null) {
if( $new !== null ) {
// no url check if the update has different value
$this->{'Pass'} = $new;
}
if( strlen($this->{'Pass'}) > 0 )
return $this->{'Pass'};
// Only try to update from path if the field is empty
$values = extract_auth_values_from_url($this->{'Path'});
if( count( $values ) == 2 ) {
$ps = $values[1];
$this->{'Pass'} = $values[1];
}
return $this->{'Pass'};
}
public function __call($fn, array $args) {
if (count($args)) {
if (is_array($this->defaults[$fn]) and $this->defaults[$fn]['type'] == 'set') {
@ -841,12 +913,10 @@ public static function getStatuses() {
*/
function getStreamHTML($options) {
if (isset($options['scale']) and $options['scale'] != '' and $options['scale'] != 'fixed') {
Debug("Have scale:" . $options['scale']);
if ( $options['scale'] != 'auto' && $options['scale'] != '0' ) {
#ZM\Warning('Setting dimensions from scale:'.$options['scale']);
if ($options['scale'] != 'auto' && $options['scale'] != '0') {
$options['width'] = reScale($this->ViewWidth(), $options['scale']).'px';
$options['height'] = reScale($this->ViewHeight(), $options['scale']).'px';
} else if ( ! ( isset($options['width']) or isset($options['height']) ) ) {
} else if (!(isset($options['width']) or isset($options['height']))) {
$options['width'] = '100%';
$options['height'] = 'auto';
}
@ -854,41 +924,35 @@ public static function getStatuses() {
$options['scale'] = 100;
# scale is empty or 100
# There may be a fixed width applied though, in which case we need to leave the height empty
if ( ! ( isset($options['width']) and $options['width'] ) ) {
if (!(isset($options['width']) and $options['width']) or ($options['width']=='auto')) {
# Havn't specified width. If we specified height, then we should
# use a width that keeps the aspect ratio, otherwise no scaling,
# no dimensions, so assume the dimensions of the Monitor
if ( ! (isset($options['height']) and $options['height']) ) {
if (!(isset($options['height']) and $options['height'])) {
# If we havn't specified any scale or dimensions, then we must be using CSS to scale it in a dynamic way. Can't make any assumptions.
#$options['width'] = $monitor->ViewWidth().'px';
#$options['height'] = $monitor->ViewHeight().'px';
}
} else {
#ZM\Warning("Have width ".$options['width']);
if ( preg_match('/^(\d+)px$/', $options['width'], $matches) ) {
if (preg_match('/^(\d+)px$/', $options['width'], $matches)) {
$scale = intval(100*$matches[1]/$this->ViewWidth());
#ZM\Warning("Scale is $scale");
if ( $scale < $options['scale'] )
if ($scale < $options['scale'])
$options['scale'] = $scale;
} else if ( preg_match('/^(\d+)%$/', $options['width'], $matches) ) {
} else if (preg_match('/^(\d+)%$/', $options['width'], $matches)) {
$scale = intval($matches[1]);
if ( $scale < $options['scale'] )
if ($scale < $options['scale'])
$options['scale'] = $scale;
} else {
$backTrace = debug_backtrace();
$file = $backTrace[1]['file'];
$line = $backTrace[1]['line'];
Warning('Invalid value for width: '.$options['width']. ' from '.$file.':'.$line);
Warning('Invalid value for width: '.$options['width']. ' from '.print_r($backTrace, true));
}
}
}
if ( ! isset($options['mode'] ) ) {
if (!isset($options['mode'])) {
$options['mode'] = 'stream';
}
if ( ! isset($options['width'] ) )
if (!isset($options['width']) or $options['width'] == 'auto')
$options['width'] = 0;
if ( ! isset($options['height'] ) )
if (!isset($options['height']) or $options['height'] == 'auto')
$options['height'] = 0;
if (!isset($options['maxfps'])) {

View File

@ -91,6 +91,7 @@ if ($action == 'save') {
'DecodingEnabled' => 0,
'JanusEnabled' => 0,
'JanusAudioEnabled' => 0,
'Janus_Use_RTSP_Restream' => 0,
'Exif' => 0,
'RTSPDescribe' => 0,
'V4LMultiBuffer' => '',

View File

@ -150,9 +150,9 @@ require_once('database.php');
require_once('logger.php');
loadConfig();
if (ZM_LOCALE_DEFAULT) {
$dateFormatter = new IntlDateFormatter(ZM_LOCALE_DEFAULT, IntlDateFormatter::SHORT, IntlDateFormatter::NONE);
$dateTimeFormatter = new IntlDateFormatter(ZM_LOCALE_DEFAULT, IntlDateFormatter::SHORT, IntlDateFormatter::LONG);
$timeFormatter = new IntlDateFormatter(ZM_LOCALE_DEFAULT, IntlDateFormatter::NONE, IntlDateFormatter::LONG);
$dateFormatter = new IntlDateFormatter(ZM_LOCALE_DEFAULT, IntlDateFormatter::SHORT, IntlDateFormatter::NONE,ZM_TIMEZONE);
$dateTimeFormatter = new IntlDateFormatter(ZM_LOCALE_DEFAULT, IntlDateFormatter::SHORT, IntlDateFormatter::LONG,ZM_TIMEZONE);
$timeFormatter = new IntlDateFormatter(ZM_LOCALE_DEFAULT, IntlDateFormatter::NONE, IntlDateFormatter::LONG,ZM_TIMEZONE);
}
if (ZM_DATE_FORMAT_PATTERN) {
$dateFormatter->setPattern(ZM_DATE_FORMAT_PATTERN);

View File

@ -720,7 +720,6 @@ function canStreamIframe() {
}
function canStreamNative() {
ZM\Debug("ZM_WEB_CAN_STREAM:".ZM_WEB_CAN_STREAM.' isInternetExplorer: ' . isInternetExplorer() . ' isOldChrome:' . isOldChrome());
// Old versions of Chrome can display the stream, but then it blocks everything else (Chrome bug 5876)
return ( ZM_WEB_CAN_STREAM == 'yes' || ( ZM_WEB_CAN_STREAM == 'auto' && (!isInternetExplorer() && !isOldChrome()) ) );
}
@ -2346,4 +2345,23 @@ function get_subnets($interface) {
return $subnets;
}
function extract_auth_values_from_url($url): array {
$protocolPrefixPos = strpos($url, '://');
if( $protocolPrefixPos === false )
return array();
$authSeparatorPos = strpos($url, '@', $protocolPrefixPos+3);
if( $authSeparatorPos === false )
return array();
$fieldsSeparatorPos = strpos($url, ':', $protocolPrefixPos+3);
if( $fieldsSeparatorPos === false || $authSeparatorPos < $fieldsSeparatorPos )
return array();
$username = substr( $url, $protocolPrefixPos+3, $fieldsSeparatorPos-($protocolPrefixPos+3) );
$password = substr( $url, $fieldsSeparatorPos+1, $authSeparatorPos-$fieldsSeparatorPos-1 );
return array( $username, $password );
}
?>

View File

@ -27,13 +27,13 @@ function tokenize(expr) {
if (character == '&' || character == '|' || character == ',') {
if (first_index != second_index) {
tokens[tokens.length] = { type: 'link', value: expr.substring(first_index, second_index) };
tokens[tokens.length] = {type: 'link', value: expr.substring(first_index, second_index)};
}
tokens[tokens.length] = { type: character, value: character };
tokens[tokens.length] = {type: character, value: character};
first_index = second_index+1;
} else if (character == '(' || character == ')') {
if (first_index != second_index) {
tokens[tokens.length] = { type: 'link', value: expr.substring(first_index, second_index) };
tokens[tokens.length] = {type: 'link', value: expr.substring(first_index, second_index)};
}
// Now check for repeats
let third = second_index+1;
@ -42,18 +42,18 @@ function tokenize(expr) {
if (expr.at(i) != character) break;
}
if (third != second_index+1) {
tokens[tokens.length] = { type: character, value: expr.substring(second_index, third) };
tokens[tokens.length] = {type: character, value: expr.substring(second_index, third)};
} else {
tokens[tokens.length] = { type: character, value: character };
tokens[tokens.length] = {type: character, value: character};
}
first_index = third;
}
second_index ++;
second_index++;
} // end for second_index
if (second_index) {
if (second_index != first_index) {
tokens[tokens.length] = { type: 'link', value: expr.substring(first_index, second_index) };
tokens[tokens.length] = {type: 'link', value: expr.substring(first_index, second_index)};
}
}
return tokens;
@ -71,10 +71,9 @@ function expr_to_ui(expr, container) {
container.html('');
var tokens = tokenize(expr);
console.log(tokens);
let term_count = count_terms(tokens);
let div;
//const term_count = count_terms(tokens);
let brackets = 0;
let used_monitorlinks = [];
const used_monitorlinks = [];
// Every monitorlink should have possible parenthesis on either side of it
if (tokens.length > 3) {
@ -148,7 +147,7 @@ function expr_to_ui(expr, container) {
const select = $j('<select></select>');
select.append('<option value="|">or</option>');
select.append('<option value="&">and</option>');
select.val(token.type);
select.val(token.type);
select.on('change', update_expr);
token.html = select;
}
@ -159,13 +158,15 @@ function expr_to_ui(expr, container) {
select.append('<option value="">Add MonitorLink</option>');
for (monitor_id in monitors) {
const monitor = monitors[monitor_id];
//if (!array_search(monitor.Id, used_monitorlinks))
//if (!array_search(monitor.Id, used_monitorlinks)) {
//select.append('<option value="' + monitor.Id + '">' + monitor.Name + ' : All Zones</option>');
//}
for ( zone_id in zones ) {
const zone = zones[zone_id];
if ( monitor.Id == zone.MonitorId ) {
if (!array_search(monitor.Id+':'+zone.Id, used_monitorlinks))
if (!array_search(monitor.Id+':'+zone.Id, used_monitorlinks)) {
select.append('<option value="' + monitor.Id+':'+zone.Id + '">' + monitor.Name + ' : ' +zone.Name + '</option>');
}
}
} // end foreach zone
} // end foreach monitor
@ -201,7 +202,7 @@ function ui_to_expr(container, expr_input) {
function parse_expression(tokens) {
if (tokens.length == 1) {
return { token: tokens[0] };
return {token: tokens[0]};
}
let left = parse_and(tokens);
@ -210,7 +211,7 @@ function parse_expression(tokens) {
}
while (token_index < tokens.length && ( tokens[token_index] == '|' || tokens[token_index] == ',')) {
var logical_or = { type: '|' };
var logical_or = {type: '|'};
token_index++;
var right = parse_and(tokens);
@ -218,7 +219,7 @@ function parse_expression(tokens) {
return null;
}
logical_or.left = left;
logical_or.left = left;
logical_or.right = right;
left = logical_or;
}
@ -236,14 +237,14 @@ function parse_and(tokens) {
while ((token_index < tokens.length) && (tokens[token_index] == '&')) {
++token_index;
var logical_and = { type: '&' };
var logical_and = {type: '&'};
right = parse_parentheses(tokens);
if (right == null) {
return null;
}
logical_and.left = left;
logical_and.left = left;
logical_and.right = right;
left = logical_and;
}
@ -264,11 +265,11 @@ function parse_parentheses(tokens) {
// Because we are parsing a left, there SHOULD be a remaining right. If not, invalid.
if (token_index == tokens.length) return null;
if (tokens[ token_index++ ] == ')') {
if (tokens[token_index++] == ')') {
return expression;
}
} else if (tokens[token_index].type == MONITORLINK) {
var link = { token: tokens[token_index] };
var link = {token: tokens[token_index]};
token_index++;
return link;
}

View File

@ -114,7 +114,13 @@ function MonitorStream(monitorData) {
}
} else if (parseInt(width) || parseInt(height)) {
if (width) {
newscale = parseInt(100*parseInt(width)/this.width);
if (width.search('px') != -1) {
newscale = parseInt(100*parseInt(width)/this.width);
} else { // %
// Set it, then get the calculated width
monitor_frame.css('width', width);
newscale = parseInt(100*parseInt(monitor_frame.width())/this.width);
}
} else if (height) {
newscale = parseInt(100*parseInt(height)/this.height);
width = parseInt(this.width * newscale / 100)+'px';
@ -124,10 +130,8 @@ function MonitorStream(monitorData) {
width = Math.round(parseInt(this.width) * newscale / 100)+'px';
height = Math.round(parseInt(this.height) * newscale / 100)+'px';
}
if (width && (width != '0px') &&
((monitor_frame[0].style.width===undefined) || (-1 == monitor_frame[0].style.width.search('%')))
) {
monitor_frame.css('width', width);
if (width && (width != '0px')) {
monitor_frame.css('width', parseInt(width));
}
if (height && height != '0px') img.style.height = height;
@ -143,6 +147,7 @@ function MonitorStream(monitorData) {
const stream_frame = $j('#monitor'+this.id);
if (!newscale) {
newscale = parseInt(100*parseInt(stream_frame.width())/this.width);
console.log("Calculated stream scale from ", stream_frame.width(), '/', this.width, '=', newscale);
}
if (img.nodeName == 'IMG') {
if (newscale > 100) newscale = 100; // we never request a larger image, as it just wastes bandwidth

View File

@ -891,6 +891,16 @@ None: No frames will be decoded, live view and thumbnails will not be available~
Attempt to enable audio in the Janus stream. Has no effect for cameras without audio support,
but can prevent a stream playing if your camera sends an audio format unsupported by the browser.'
),
'FUNCTION_JANUS_PROFILE_OVERRIDE' => array(
'Help' => '
Manually set a Profile-ID, which can force a browser to try to play a given stream. Try "42e01f"
for a universally supported value, or leave this blank to use the Profile-ID specified by the source.'
),
'FUNCTION_JANUS_USE_RTSP_RESTREAM' => array(
'Help' => '
If your camera will not work under Janus with any other options, enable this to use the ZoneMinder
RTSP restream as the Janus source.'
),
'ImageBufferCount' => array(
'Help' => '
Number of raw images available in /dev/shm. Currently should be set in the 3-5 range. Used for live viewing.'

File diff suppressed because it is too large Load Diff

View File

@ -22,41 +22,29 @@
*/
@font-face {
font-family: 'Material Icons';
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: url(../fonts/MaterialIcons-Regular.eot); /* For IE6-8 */
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url(../fonts/MaterialIcons-Regular.woff2) format('woff2'),
url(../fonts/MaterialIcons-Regular.woff) format('woff'),
url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
font-display: block;
src: url("../fonts/material-icons.woff2") format("woff2"), url("../fonts/material-icons.woff") format("woff");
}
.material-icons {
vertical-align:middle;
font-family: 'Material Icons';
font-family: "Material Icons";
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */
display: inline-block;
font-size: 24px;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
text-rendering: optimizeLegibility;
font-feature-settings: "liga";
vertical-align: middle;
}
.material-icons.md-18 { font-size: 18px; }
@ -142,6 +130,7 @@ label {
margin: 0 4px;
}
a.btn,
button.btn {
line-height: 1;
font-size: 18px;

View File

@ -18,6 +18,7 @@
.ptzControls .pantiltPanel button {
border: none;
margin: 0;
padding: 0;
}
.ptzControls .controlsPanel {
@ -31,6 +32,7 @@
.ptzControls .controlsPanel .arrowControl {
width: 60px;
margin: 0 4px;
padding: 0;
}
.ptzControls .controlsPanel .arrowControl button.longArrowBtn {
@ -41,6 +43,7 @@
width: 32px;
height: 48px;
margin: 0 auto;
padding: 0;
cursor: pointer;
}

View File

@ -19,9 +19,16 @@ input[name="newMonitor[ControlAddress]"],
input[name="newMonitor[ONVIF_URL]"],
input[name="newMonitor[ONVIF_Username]"],
input[name="newMonitor[ONVIF_Password]"],
input[name="newMonitor[ONVIF_Options]"] {
input[name="newMonitor[ONVIF_Options]"],
input[name="newMonitor[MQTT_Subscriptions]"] {
width: 100%;
min-width: 240px;
}
input[name="newMonitor[ONVIF_URL]"] {
min-width: 340px;
}
input[name="newMonitor[ONVIF_Username]"],
input[name="newMonitor[ONVIF_Password]"],
input[name="newMonitor[LabelFormat]"]{
min-width: 240px;
}
@ -62,3 +69,17 @@ tr td input[type="checkbox"],
tr td input[type="radio"] {
margin: 6px 0;
}
.tab-content,
.tab-content .active {
width: 100%;
}
.tab-content .active table {
width: 100%;
table-layout: fixed;
}
.tab-content .active table tr {
height: 32px;
}
.tab-content .active table td.text-right {
width: 245px;
}

View File

@ -103,7 +103,7 @@ window.addEventListener("DOMContentLoaded", function onSkinDCL() {
// 'data-on-click-this' calls the global function in the attribute value with the element when a click happens.
function dataOnClickThis() {
document.querySelectorAll("a[data-on-click-this], button[data-on-click-this], input[data-on-click-this]").forEach(function attachOnClick(el) {
document.querySelectorAll("a[data-on-click-this], button[data-on-click-this], input[data-on-click-this], span[data-on-click-this]").forEach(function attachOnClick(el) {
var fnName = el.getAttribute("data-on-click-this");
if ( !window[fnName] ) {
console.error("Nothing found to bind to " + fnName + " on element " + el.name);
@ -1003,3 +1003,18 @@ function closeFullscreen() {
document.msExitFullscreen();
}
}
function toggle_password_visibility(element) {
const input = document.getElementById(element.getAttribute('data-password-input'));
if (!input) {
console.log("Input not found! " + element.getAttribute('data-password-input'));
return;
}
if (element.innerHTML=='visibility') {
input.type = 'text';
element.innerHTML = 'visibility_off';
} else {
input.type = 'password';
element.innerHTML='visibility';
}
}

View File

@ -25,24 +25,24 @@
global $user;
?>
var AJAX_TIMEOUT = <?php echo ZM_WEB_AJAX_TIMEOUT ?>;
var navBarRefresh = <?php echo 1000*ZM_WEB_REFRESH_NAVBAR ?>;
var currentView = '<?php echo $view ?>';
const AJAX_TIMEOUT = <?php echo ZM_WEB_AJAX_TIMEOUT ?>;
const navBarRefresh = <?php echo 1000*ZM_WEB_REFRESH_NAVBAR ?>;
const currentView = '<?php echo $view ?>';
var exportProgressString = '<?php echo addslashes(translate('Exporting')) ?>';
var exportFailedString = '<?php echo translate('ExportFailed') ?>';
var exportSucceededString = '<?php echo translate('ExportSucceeded') ?>';
var cancelString = '<?php echo translate('Cancel') ?>';
const exportProgressString = '<?php echo addslashes(translate('Exporting')) ?>';
const exportFailedString = '<?php echo translate('ExportFailed') ?>';
const exportSucceededString = '<?php echo translate('ExportSucceeded') ?>';
const cancelString = '<?php echo translate('Cancel') ?>';
<?php
/* We can't trust PHP_SELF on a path like /index.php/"%3E%3Cimg src=x onerror=prompt('1');%3E which
will still load index.php but will include the arbitrary payload after `.php/`. To mitigate this,
try to avoid using PHP_SELF but here I try to replace everything after '.php'. */ ?>
var thisUrl = '<?php echo ZM_BASE_URL.preg_replace('/\.php.*$/i', '.php', $_SERVER['PHP_SELF']) ?>';
var skinPath = '<?php echo ZM_SKIN_PATH ?>';
var serverId = '<?php echo defined('ZM_SERVER_ID') ? ZM_SERVER_ID : '' ?>';
const thisUrl = '<?php echo ZM_BASE_URL.preg_replace('/\.php.*$/i', '.php', $_SERVER['PHP_SELF']) ?>';
const skinPath = '<?php echo ZM_SKIN_PATH ?>';
const serverId = '<?php echo defined('ZM_SERVER_ID') ? ZM_SERVER_ID : '' ?>';
var canView = {};
var canEdit = {};
const canView = {};
const canEdit = {};
<?php
$perms = array('Stream', 'Events', 'Control', 'Monitors', 'Groups', 'Snapshots', 'System', 'Devices');
foreach ( $perms as $perm ) {
@ -53,8 +53,8 @@ foreach ( $perms as $perm ) {
}
?>
var ANIMATE_THUMBS = <?php echo ZM_WEB_ANIMATE_THUMBS?'true':'false' ?>;
var SCALE_BASE = <?php echo SCALE_BASE ?>;
const ANIMATE_THUMBS = <?php echo ZM_WEB_ANIMATE_THUMBS?'true':'false' ?>;
const SCALE_BASE = <?php echo SCALE_BASE ?>;
var refreshParent = <?php
if ( ! empty($refreshParent) ) {
@ -80,7 +80,7 @@ if ( ( ! empty($closePopup) ) and ( $closePopup == true ) ) {
var focusWindow = <?php echo !empty($focusWindow)?'true':'false' ?>;
var imagePrefix = "<?php echo '?view=image&eid=' ?>";
const imagePrefix = '<?php echo '?view=image&eid=' ?>';
var auth_hash = '<?php echo generateAuthHash(ZM_AUTH_HASH_IPS) ?>';
var auth_relay = '<?php echo get_auth_relay() ?>';
@ -121,7 +121,7 @@ const CMD_QUERY = <?php echo CMD_QUERY ?>;
const CMD_QUIT = <?php echo CMD_QUIT ?>;
const CMD_MAXFPS = <?php echo CMD_MAXFPS ?>;
var stateStrings = new Array();
const stateStrings = new Array();
stateStrings[STATE_UNKNOWN] = "<?php echo translate('Unknown') ?>";
stateStrings[STATE_IDLE] = "<?php echo translate('Idle') ?>";
stateStrings[STATE_PREALARM] = "<?php echo translate('Prealarm') ?>";

View File

@ -173,6 +173,7 @@ if ( $Event->Id() and !file_exists($Event->Path()) )
<button id="statsBtn" class="btn btn-normal" data-toggle="tooltip" data-placement="top" title="<?php echo translate('Stats') ?>" ><i class="fa fa-info"></i></button>
<button id="framesBtn" class="btn btn-normal" data-toggle="tooltip" data-placement="top" title="<?php echo translate('Frames') ?>" ><i class="fa fa-picture-o"></i></button>
<button id="deleteBtn" class="btn btn-danger" data-toggle="tooltip" data-placement="top" title="<?php echo translate('Delete') ?>"><i class="fa fa-trash"></i></button>
<a href="?view=montagereview&live=0&current=<?php echo urlencode($Event->StartDateTime()) ?>" class="btn btn-normal" title="<?php echo translate('Montage Review') ?>"><i class="material-icons md-18">grid_view</i></a>
<?php
if (canView('System')) { ?>
<button id="toggleZonesButton" class="btn btn-<?php echo $showZones?'normal':'secondary'?>" title="<?php echo translate(($showZones?'Hide':'Show').' Zones')?>" ><span class="material-icons"><?php echo $showZones?'layers_clear':'layers'?></span</button>

View File

@ -28,5 +28,3 @@ monitorData[monitorData.length] = {
<?php
} // end foreach monitor
?>
var SCALE_BASE = <?php echo SCALE_BASE ?>;

View File

@ -48,7 +48,7 @@ function vjsReplay() {
break;
case 'all':
if ( nextEventId == 0 ) {
var overLaid = $j("#videoobj");
const overLaid = $j('#videoobj');
overLaid.append('<p class="vjsMessage" style="height: '+overLaid.height()+'px; line-height: '+overLaid.height()+'px;">No more events</p>');
} else {
if (!eventData.EndDateTime) {
@ -56,20 +56,26 @@ function vjsReplay() {
streamNext(true);
return;
}
var endTime = Date.parse(eventData.EndDateTime).getTime();
var nextStartTime = nextEventStartTime.getTime(); //nextEventStartTime.getTime() is a mootools workaround, highjacks Date.parse
const date = Date.parse(eventData.EndDateTime);
if (!date) {
console.error('Got no date from ', eventData);
streamNext(true);
return;
}
const endTime = date.getTime();
const nextStartTime = nextEventStartTime.getTime(); //nextEventStartTime.getTime() is a mootools workaround, highjacks Date.parse
if ( nextStartTime <= endTime ) {
streamNext(true);
return;
}
vid.pause();
var overLaid = $j("#videoobj");
const overLaid = $j("#videoobj");
overLaid.append('<p class="vjsMessage" style="height: '+overLaid.height()+'px; line-height: '+overLaid.height()+'px;"></p>');
var gapDuration = (new Date().getTime()) + (nextStartTime - endTime);
var messageP = $j('.vjsMessage');
var x = setInterval(function() {
var now = new Date().getTime();
var remainder = new Date(Math.round(gapDuration - now)).toISOString().substr(11, 8);
const gapDuration = (new Date().getTime()) + (nextStartTime - endTime);
const messageP = $j('.vjsMessage');
const x = setInterval(function() {
const now = new Date().getTime();
const remainder = new Date(Math.round(gapDuration - now)).toISOString().substr(11, 8);
messageP.html(remainder + ' to next event.');
if ( remainder < 0 ) {
clearInterval(x);

View File

@ -13,8 +13,6 @@
global $popup;
?>
var SCALE_BASE = <?php echo SCALE_BASE ?>;
//
// PHP variables to JS
//

View File

@ -7,8 +7,6 @@
var scale = '<?php echo validJsStr($scale); ?>';
var SCALE_BASE = <?php echo SCALE_BASE ?>;
var eid = <?php echo $eid ?>;
var fid = <?php echo $fid ?>;
var record_event_stats = <?php echo ZM_RECORD_EVENT_STATS ?>;

View File

@ -42,7 +42,11 @@ function ajaxRequest(params) {
}
function processRows(rows) {
$j.each(rows, function(ndx, row) {
row.Message = decodeURIComponent(row.Message);
try {
row.Message = decodeURIComponent(row.Message);
} catch (e) {
// ignore errors
}
});
return rows;
}

View File

@ -262,18 +262,26 @@ function initPage() {
window.location.assign('?view=console');
});
//manage the Janus audio check
//manage the Janus settings div
if (document.getElementsByName("newMonitor[JanusEnabled]")[0].checked) {
document.getElementById("FunctionJanusAudioEnabled").hidden = false;
document.getElementById("FunctionJanusProfileOverride").hidden = false;
document.getElementById("FunctionJanusUseRTSPRestream").hidden = false;
} else {
document.getElementById("FunctionJanusAudioEnabled").hidden = true;
document.getElementById("FunctionJanusProfileOverride").hidden = true;
document.getElementById("FunctionJanusUseRTSPRestream").hidden = true;
}
document.getElementsByName("newMonitor[JanusEnabled]")[0].addEventListener('change', function() {
if (this.checked) {
document.getElementById("FunctionJanusAudioEnabled").hidden = false;
document.getElementById("FunctionJanusProfileOverride").hidden = false;
document.getElementById("FunctionJanusUseRTSPRestream").hidden = false;
} else {
document.getElementById("FunctionJanusAudioEnabled").hidden = true;
document.getElementById("FunctionJanusProfileOverride").hidden = true;
document.getElementById("FunctionJanusUseRTSPRestream").hidden = true;
}
});
@ -294,6 +302,20 @@ function initPage() {
}
});
const monitorPath = document.getElementsByName("newMonitor[Path]")[0];
monitorPath.addEventListener('keyup', change_Path); // on edit sync path -> user & pass
monitorPath.addEventListener('blur', change_Path); // remove fields from path if user & pass equal on end of edit
const monitorUser = document.getElementsByName("newMonitor[User]");
if ( monitorUser.length > 0 ) {
monitorUser[0].addEventListener('blur', change_Path); // remove fields from path if user & pass equal
}
const monitorPass = document.getElementsByName("newMonitor[Pass]");
if ( monitorPass.length > 0 ) {
monitorPass[0].addEventListener('blur', change_Path); // remove fields from path if user & pass equal
}
if ( parseInt(ZM_OPT_USE_GEOLOCATION) ) {
if ( window.L ) {
const form = document.getElementById('contentForm');
@ -335,6 +357,64 @@ function initPage() {
updateLinkedMonitorsUI();
} // end function initPage()
function change_Path(event) {
var pathInput = document.getElementsByName("newMonitor[Path]")[0];
var protoPrefixPos = pathInput.value.indexOf('://');
if ( protoPrefixPos == -1 ) {
return;
}
// check the formatting of the url
var authSeparatorPos = pathInput.value.indexOf( '@', protoPrefixPos+3 );
if ( authSeparatorPos == -1 ) {
console.warn('ignoring URL incorrectly formatted, missing "@"');
return;
}
var fieldsSeparatorPos = pathInput.value.indexOf( ':', protoPrefixPos+3 );
if ( authSeparatorPos == -1 || fieldsSeparatorPos >= authSeparatorPos ) {
console.warn('ignoring URL incorrectly formatted, missing ":"');
return;
}
var usernameValue = pathInput.value.substring( protoPrefixPos+3, fieldsSeparatorPos );
var passwordValue = pathInput.value.substring( fieldsSeparatorPos+1, authSeparatorPos );
if ( usernameValue.length == 0 || passwordValue.length == 0 ) {
console.warn('ignoring URL incorrectly formatted, empty username or password');
return;
}
// get the username / password inputs
var userInput = document.getElementsByName("newMonitor[User]");
var passInput = document.getElementsByName("newMonitor[Pass]");
if (userInput.length != 1 || passInput.length != 1) {
return;
}
// on editing update the fields only if they are empty or a prefix of the new value
if ( event.type != 'blur' ) {
if ( userInput[0].value.length == 0 || usernameValue.indexOf(userInput[0].value) == 0 ||
userInput[0].value.indexOf(usernameValue) == 0 ) {
userInput[0].value = usernameValue;
}
if ( passInput[0].value.length == 0 || passwordValue.indexOf(passInput[0].value) == 0 ||
passInput[0].value.indexOf(passwordValue) == 0 ) {
passInput[0].value = passwordValue;
}
return;
}
// on leaving the input sync the values and remove it from the url
// only if they already match (to not overwrite already present values)
if ( userInput[0].value == usernameValue && passInput[0].value == passwordValue ) {
pathInput.value = pathInput.value.substring(0, protoPrefixPos+3) + pathInput.value.substring(authSeparatorPos+1, pathInput.value.length);
}
}
function change_WebColour() {
$j('#WebSwatch').css(
'backgroundColor',

View File

@ -1,3 +1,4 @@
const monitors = new Array();
const VIEWING = 0;
const EDITING = 1;
@ -65,14 +66,9 @@ function selectLayout(new_layout_id) {
//$j('#height').val('auto');
}
var width = $j('#width').val();
var height = $j('#height').val();
var scale = $j('#scale').val();
for (var i = 0, length = monitors.length; i < length; i++) {
var monitor = monitors[i];
monitor.setScale(scale, width, height);
for (let i = 0, length = monitors.length; i < length; i++) {
monitors[i].setStreamScale();
} // end foreach monitor
console.log("Done selectLayout");
} // end function selectLayout(element)
function changeHeight() {
@ -235,7 +231,6 @@ function handleClick(evt) {
}
}
const monitors = new Array();
function initPage() {
$j("#hdrbutton").click(function() {
$j("#flipMontageHeader").slideToggle("slow");
@ -265,8 +260,8 @@ function initPage() {
// If you click on the navigation links, shut down streaming so the browser can process it
document.querySelectorAll('#main-header-nav a').forEach(function(el) {
el.onclick = function() {
for (var i = 0, length = monitors.length; i < length; i++) {
monitors[i].kill();
for (let i = 0, length = monitors.length; i < length; i++) {
if (monitors[i]) monitors[i].kill();
}
};
});

View File

@ -1,27 +1,28 @@
function evaluateLoadTimes() {
if (liveMode != 1 && currentSpeed == 0) return; // don't evaluate when we are not moving as we can do nothing really fast.
// Only consider it a completed event if we load ALL monitors, then zero all and start again
var start=0;
var end=0;
if ( liveMode != 1 && currentSpeed == 0 ) return; // don't evaluate when we are not moving as we can do nothing really fast.
for ( var i = 0; i < monitorIndex.length; i++ ) {
if ( monitorName[i] > "" ) {
let start=0;
let end=0;
for (let i = 0; i < monitorIndex.length; i++) {
if (monitorName[i] > '') {
if ( monitorLoadEndTimems[i] == 0 ) return; // if we have a monitor with no time yet just wait
if ( start == 0 || start > monitorLoadStartTimems[i] ) start = monitorLoadStartTimems[i];
if ( end == 0 || end < monitorLoadEndTimems[i] ) end = monitorLoadEndTimems[i];
}
}
if ( start == 0 || end == 0 ) return; // we really should not get here
for ( var i=0; i < numMonitors; i++ ) {
var monId = monitorPtr[i];
for (let i=0; i < numMonitors; i++) {
const monId = monitorPtr[i];
monitorLoadStartTimems[monId] = 0;
monitorLoadEndTimems[monId] = 0;
}
freeTimeLastIntervals[imageLoadTimesEvaluated++] = 1 - ((end - start)/currentDisplayInterval);
if ( imageLoadTimesEvaluated < imageLoadTimesNeeded ) return;
var avgFrac=0;
for ( var i=0; i < imageLoadTimesEvaluated; i++ ) {
if (imageLoadTimesEvaluated < imageLoadTimesNeeded) return;
let avgFrac=0;
for (let i=0; i < imageLoadTimesEvaluated; i++) {
avgFrac += freeTimeLastIntervals[i];
}
avgFrac = avgFrac / imageLoadTimesEvaluated;
@ -45,8 +46,59 @@ function evaluateLoadTimes() {
$j('#fps').text("Display refresh rate is " + (1000 / currentDisplayInterval).toFixed(1) + " per second, avgFrac=" + avgFrac.toFixed(3) + ".");
} // end evaluateLoadTimes()
function findEventByTime(arr, x) {
let start=0;
let end=arr.length-1;
// Iterate while start not meets end
while (start <= end) {
// Find the mid index
const mid = Math.floor((start + end)/2);
// If element is present at mid, return True
//console.log(mid, arr[mid], x);
if (arr[mid].StartTimeSecs <= x && arr[mid].EndTimeSecs >= x) {
return arr[mid];
} else {
// Else look in left or right half accordingly
if (arr[mid].StartTimeSecs < x) {
start = mid + 1;
} else {
end = mid - 1;
}
}
}
return false;
}
function findFrameByTime(arr, x) {
let start=0;
let end=arr.length-1;
// Iterate while start not meets end
while (start <= end) {
// Find the mid index
const mid = Math.floor((start + end)/2);
// If element is present at mid, return True
if (arr[mid].StartTimeSecs <= x && arr[mid].EndTimeSec >= x) {
return true;
// Else look in left or right half accordingly
} else if (arr[mid].StartTimeSecs < x) {
start = mid + 1;
} else {
end = mid - 1;
}
}
return false;
}
function getFrame(monId, time, last_Frame) {
if ( last_Frame ) {
if (last_Frame) {
if (
(last_Frame.TimeStampSecs <= time) &&
(last_Frame.EndTimeStampSecs >= time)
@ -55,72 +107,70 @@ function getFrame(monId, time, last_Frame) {
}
}
var events_for_monitor = events_by_monitor_id[monId];
if ( !events_for_monitor ) {
if (!events_by_monitor_id[monId]) {
// Need to load them?
return;
}
const events_for_monitor = events_by_monitor_id[monId].map((x)=>events[x]);
if (!events_for_monitor.length) {
//console.log("No events for monitor " + monId);
return;
}
var Frame = null;
for ( var i = 0; i < events_for_monitor.length; i++ ) {
//for ( var event_id_idx in events_for_monitor ) {
var event_id = events_for_monitor[i];
// Search for the event matching this time. Would be more efficient if we had events indexed by monitor
e = events[event_id];
if ( !e ) {
console.log("No event found for " + event_id);
break;
}
if ( e.MonitorId != monId || e.StartTimeSecs > time || e.EndTimeSecs < time ) {
//console.log("Event not for " + time);
continue;
}
if ( !e.FramesById ) {
console.log("No FramesById for event " + event_id);
return;
}
var duration = e.EndTimeSecs - e.StartTimeSecs;
// I think this is an estimate to jump near the desired frame.
var frame = parseInt((time - e.StartTimeSecs)/(duration)*Object.keys(e.FramesById).length)+1;
//console.log("frame_id for " + time + " is " + frame);
// Need to get frame by time, not some fun calc that assumes frames have the same length.
// Frames are sorted in descreasing order (or not sorted).
// This is likely not efficient. Would be better to start at the last frame viewed, see if it is still relevant
// Then move forward or backwards as appropriate
for ( var frame_id in e.FramesById ) {
if ( 0 ) {
if ( frame == 0 ) {
console.log("Found frame for time " + time);
console.log(Frame);
Frame = e.FramesById[frame_id];
break;
}
frame --;
continue;
}
if (
e.FramesById[frame_id].TimeStampSecs == time ||
(
e.FramesById[frame_id].TimeStampSecs < time &&
(
(!e.FramesById[frame_id].NextTimeStampSecs) || // only if event.EndTime is null
(e.FramesById[frame_id].NextTimeStampSecs > time)
)
)
) {
Frame = e.FramesById[frame_id];
let Frame = null;
let Event = findEventByTime(events_for_monitor, time);
if (Event === false) {
// This might be better with a binary search
for (let i = 0; i < events_for_monitor.length; i++) {
const event_id = events_for_monitor[i].Id;
// Search for the event matching this time. Would be more efficient if we had events indexed by monitor
const e = events[event_id];
if (!e) {
console.error('No event found for ', event_id);
break;
}
if (e.StartTimeSecs <= time && e.EndTimeSecs >= time) {
Event = e;
break;
}
} // end foreach frame in the event.
if ( !Frame ) {
console.log("Didn't find frame for " + time);
return null;
}
} // end foreach event
if (Event) {
console.log("Failed to find event for ", time, " but found it using linear search");
}
}
if (!Event) return;
if (!Event.FramesById) {
console.log('No FramesById for event ', Event);
return;
}
// Need to get frame by time, not some fun calc that assumes frames have the same length.
// Frames are sorted in descreasing order (or not sorted).
// This is likely not efficient. Would be better to start at the last frame viewed, see if it is still relevant
// Then move forward or backwards as appropriate
for (const frame_id in Event.FramesById) {
// Again need binary search
if (
Event.FramesById[frame_id].TimeStampSecs == time ||
(
Event.FramesById[frame_id].TimeStampSecs < time &&
(
(!Event.FramesById[frame_id].NextTimeStampSecs) || // only if event.EndTime is null
(Event.FramesById[frame_id].NextTimeStampSecs > time)
)
)
) {
Frame = Event.FramesById[frame_id];
break;
}
} // end foreach frame in the event.
if (!Frame) {
console.log("Didn't find frame for " + time);
}
return Frame;
}
@ -157,7 +207,6 @@ function getImageSource(monId, time) {
}
} else {
frame_id = Frame.FrameId;
console.log("No NextFrame");
}
Event = events[Frame.EventId];

View File

@ -225,8 +225,8 @@ var minTime='$minTime';
var maxTime='$maxTime';
";
echo 'var rangeTimeSecs='.($maxTimeSecs - $minTimeSecs + 1).";\n";
if ( isset($defaultCurrentTime) )
echo 'var currentTimeSecs=parseInt('.strtotime($defaultCurrentTime).");\n";
if ( isset($defaultCurrentTimeSecs) )
echo 'var currentTimeSecs=parseInt('.$defaultCurrentTimeSecs.");\n";
else
echo 'var currentTimeSecs=parseInt('.(($minTimeSecs + $maxTimeSecs)/2).");\n";

View File

@ -55,7 +55,6 @@ monitorData[monitorData.length] = {
} // end foreach monitor
?>
var SCALE_BASE = <?php echo SCALE_BASE ?>;
var scale = '<?php echo $scale ?>';
var statusRefreshTimeout = <?php echo 1000*ZM_WEB_REFRESH_STATUS ?>;

View File

@ -96,8 +96,6 @@ var deleteString = "<?php echo translate('Delete') ?>";
//
var SCALE_BASE = <?php echo SCALE_BASE ?>;
const POPUP_ON_ALARM = false;
var streamMode = "<?php echo $streamMode ?>";

View File

@ -370,6 +370,7 @@ if ( $monitor->Type() != 'WebSite' ) {
$tabs['x10'] = translate('X10');
$tabs['misc'] = translate('Misc');
$tabs['location'] = translate('Location');
$tabs['mqtt'] = translate('MQTT');
}
if (isset($_REQUEST['tab']) and isset($tabs[$_REQUEST['tab']]) ) {
@ -572,7 +573,10 @@ switch ($name) {
</tr>
<tr>
<td class="text-right pr-3"><?php echo translate('Password') ?></td>
<td><input type="text" name="newMonitor[ONVIF_Password]" value="<?php echo validHtmlStr($monitor->ONVIF_Password()) ?>"/></td>
<td>
<input type="password" id="newMonitor[ONVIF_Password]" name="newMonitor[ONVIF_Password]" value="<?php echo validHtmlStr($monitor->ONVIF_Password()) ?>"/>
<span class="material-icons md-18" data-on-click-this="toggle_password_visibility" data-password-input="newMonitor[ONVIF_Password]">visibility</span>
</td>
</tr>
<tr>
<td class="text-right pr-3"><?php echo translate('ONVIF_Options') ?></td>
@ -629,7 +633,7 @@ $localMethods = array(
if (!ZM_HAS_V4L2)
unset($localMethods['v4l2']);
echo htmlSelect('newMonitor[Method]', $localMethods,
((count($localMethods)<=1) ? array_key_first($localMethods) : $monitor->Method()),
((count($localMethods)==1) ? array_keys($localMethods)[0] : $monitor->Method()),
array('data-on-change'=>'submitTab', 'data-tab-name'=>$tab) );
?></td>
</tr>
@ -683,11 +687,22 @@ include('_monitor_source_nvsocket.php');
</tr>
<tr>
<td class="text-right pr-3"><?php echo translate('Password') ?></td>
<td><input type="text" name="newMonitor[Pass]" value="<?php echo validHtmlStr($monitor->Pass()) ?>"/></td>
<td>
<input type="password" id="newMonitor[Pass]" name="newMonitor[Pass]" value="<?php echo validHtmlStr($monitor->Pass()) ?>"/>
<span class="material-icons md-18" data-on-click-this="toggle_password_visibility" data-password-input="newMonitor[Pass]">visibility</span>
</td>
</tr>
<?php
} else if ( $monitor->Type() == 'Remote' ) {
?>
<tr><td class="text-right pr-3"><?php echo 'Username' ?></td><td><input type="text" name="newMonitor[User]" value="<?php echo validHtmlStr($monitor->User()) ?>"/></td></tr>
<tr>
<td class="text-right pr-3"><?php echo 'Password' ?></td>
<td>
<input type="password" id="newMonitor[Pass]" name="newMonitor[Pass]" value="<?php echo validHtmlStr($monitor->Pass()) ?>"/>
<span class="material-icons md-18" data-on-click-this="toggle_password_visibility" data-password-input="newMonitor[Pass]">visibility</span>
</td>
</tr>
<tr>
<td class="text-right pr-3"><?php echo translate('RemoteProtocol') ?></td>
<td><?php echo htmlSelect('newMonitor[Protocol]', $remoteProtocols, $monitor->Protocol(), "updateMethods( this );if(this.value=='rtsp'){\$('RTSPDescribe').setStyle('display','table-row');}else{\$('RTSPDescribe').hide();}" ); ?></td>
@ -721,7 +736,13 @@ include('_monitor_source_nvsocket.php');
?>
<tr><td class="text-right pr-3"><?php echo 'URL' ?></td><td><input type="text" name="newMonitor[Path]" value="<?php echo validHtmlStr($monitor->Path()) ?>"/></td></tr>
<tr><td class="text-right pr-3"><?php echo 'Username' ?></td><td><input type="text" name="newMonitor[User]" value="<?php echo validHtmlStr($monitor->User()) ?>"/></td></tr>
<tr><td class="text-right pr-3"><?php echo 'Password' ?></td><td><input type="text" name="newMonitor[Pass]" value="<?php echo validHtmlStr($monitor->Pass()) ?>"/></td></tr>
<tr>
<td class="text-right pr-3"><?php echo 'Password' ?></td>
<td>
<input type="password" id="newMonitor[Pass]" name="newMonitor[Pass]" value="<?php echo validHtmlStr($monitor->Pass()) ?>"/>
<span class="material-icons md-18" data-on-click-this="toggle_password_visibility" data-password-input="newMonitor[Pass]">visibility</span>
</td>
</tr>
<?php
} elseif ( $monitor->Type() == 'WebSite' ) {
?>
@ -748,6 +769,14 @@ include('_monitor_source_nvsocket.php');
<td class="text-right pr-3"><?php echo translate('SourcePath') ?></td>
<td><input type="text" name="newMonitor[Path]" value="<?php echo validHtmlStr($monitor->Path()) ?>" /></td>
</tr>
<tr><td class="text-right pr-3"><?php echo 'Username' ?></td><td><input type="text" name="newMonitor[User]" value="<?php echo validHtmlStr($monitor->User()) ?>"/></td></tr>
<tr>
<td class="text-right pr-3"><?php echo 'Password' ?></td>
<td>
<input type="password" id="newMonitor[Pass]" name="newMonitor[Pass]" value="<?php echo validHtmlStr($monitor->Pass()) ?>"/>
<span class="material-icons md-18" data-on-click-this="toggle_password_visibility" data-password-input="newMonitor[Pass]">visibility</span>
</td>
</tr>
<tr>
<td class="text-right pr-3">
<?php echo translate('RemoteMethod'); echo makeHelpLink('OPTIONS_RTSPTrans') ?></td>
@ -1125,6 +1154,7 @@ $videowriter_encoders = array(
'hevc_vaapi' => 'hevc_vaapi',
'libvpx-vp9' => 'libvpx-vp9',
'libsvtav1' => 'libsvtav1',
'libaom-av1' => 'libaom-av1'
);
echo htmlSelect('newMonitor[Encoder]', $videowriter_encoders, $monitor->Encoder());?></td></tr>
<tr class="OutputContainer">
@ -1187,6 +1217,26 @@ echo htmlSelect('newMonitor[OutputContainer]', $videowriter_containers, $monitor
if ( isset($OLANG['FUNCTION_JANUS_AUDIO_ENABLED']) ) {
echo '<div class="form-text">'.$OLANG['FUNCTION_JANUS_AUDIO_ENABLED']['Help'].'</div>';
}
?>
</td>
</tr>
<tr id="FunctionJanusProfileOverride">
<td class="text-right pr-3"><?php echo translate('Janus Profile-ID Override') ?></td>
<td><input type="text" name="newMonitor[Janus_Profile_Override]" value="<?php echo $monitor->Janus_Profile_Override()?>"/>
<?php
if ( isset($OLANG['FUNCTION_JANUS_PROFILE_OVERRIDE']) ) {
echo '<div class="form-text">'.$OLANG['FUNCTION_JANUS_PROFILE_OVERRIDE']['Help'].'</div>';
}
?>
</td>
</tr>
<tr id="FunctionJanusUseRTSPRestream">
<td class="text-right pr-3"><?php echo translate('Janus Use RTSP Restream') ?></td>
<td><input type="checkbox" name="newMonitor[Janus_Use_RTSP_Restream]" value="1"<?php echo $monitor->Janus_Use_RTSP_Restream() ? ' checked="checked"' : '' ?>/>
<?php
if ( isset($OLANG['FUNCTION_JANUS_USE_RTSP_RESTREAM']) ) {
echo '<div class="form-text">'.$OLANG['FUNCTION_JANUS_USE_RTSP_RESTREAM']['Help'].'</div>';
}
?>
</td>
</tr>
@ -1459,6 +1509,18 @@ echo htmlSelect('newMonitor[ReturnLocation]', $return_options, $monitor->ReturnL
<tr>
<td colspan="2"><div id="LocationMap" style="height: 500px; width: 500px;"></div></td>
</tr>
<?php
break;
case 'mqtt':
?>
<tr>
<td class="text-right pr-3"><?php echo translate('MQTT Enabled') ?></td>
<td><?php echo html_radio('newMonitor[MQTT_Enabled]', array('1'=>translate('Enabled'), '0'=>translate('Disabled')), $monitor->MQTT_Enabled()) ?></td>
</tr>
<tr>
<td class="text-right pr-3"><?php echo translate('MQTT Subscriptions') ?></td>
<td><input type="text" name="newMonitor[MQTT_Subscriptions]" value="<?php echo $monitor->MQTT_Subscriptions() ?>" /></td>
</tr>
<?php
break;
default :

View File

@ -29,12 +29,14 @@
// Valid query string:
//
// &maxTime, minTime = string formats (locale) of starting and ending time for history (pass both or none), default = last hour
// if not specified, but current is, then should center 1 hour on current
//
// &current = string format of time, where the slider is positioned first in history mode (normally only used in reloads, default = half scale)
// also used when jumping from event view to montagereview
//
// &speed = one of the valid speeds below (see $speeds in php section, default = 1.0)
//
// &scale = image sie scale (.1 to 1.0, or 1.1 = fit, default = fit)
// &scale = image size scale (.1 to 1.0, or 1.1 = fit, default = fit)
//
// &live=1 whether to start in live mode, 1 = yes, 0 = no
//
@ -59,12 +61,47 @@ include('_monitor_filters.php');
$filter_bar = ob_get_contents();
ob_end_clean();
// Parse input parameters -- note for future, validate/clean up better in case we don't get called from self.
// Live overrides all the min/max stuff but it is still processed
// The default (nothing at all specified) is for 1 hour so we do not read the whole database
if (isset($_REQUEST['current'])) {
$defaultCurrentTime = validHtmlStr($_REQUEST['current']);
$defaultCurrentTimeSecs = strtotime($defaultCurrentTime);
}
if ( !isset($_REQUEST['minTime']) && !isset($_REQUEST['maxTime']) ) {
if (isset($defaultCurrentTimeSecs)) {
$minTime = date('c', $defaultCurrentTimeSecs - 1800);
$maxTime = date('c', $defaultCurrentTimeSecs + 1800);
} else {
$time = time();
$maxTime = date('c', $time);
$minTime = date('c', $time - 3600);
}
} else {
if (isset($_REQUEST['minTime']))
$minTime = validHtmlStr($_REQUEST['minTime']);
if (isset($_REQUEST['maxTime']))
$maxTime = validHtmlStr($_REQUEST['maxTime']);
}
// AS a special case a "all" is passed in as an extreme interval - if so, clear them here and let the database query find them
if ( (strtotime($maxTime) - strtotime($minTime))/(365*24*3600) > 30 ) {
// test years
$minTime = null;
$maxTime = null;
}
$filter = array();
if ( isset($_REQUEST['filter']) ) {
if (isset($_REQUEST['filter'])) {
$filter = $_REQUEST['filter'];
# Try to guess min/max time from filter
foreach ( $filter['Query'] as $term ) {
foreach ($filter['Query'] as $term) {
if ( $term['attr'] == 'StartDateTime' ) {
if ( $term['op'] == '<=' or $term['op'] == '<' ) {
$maxTime = $term['val'];
@ -160,29 +197,6 @@ if ( isset($_SESSION['archive_status']) ) {
}
}
// Parse input parameters -- note for future, validate/clean up better in case we don't get called from self.
// Live overrides all the min/max stuff but it is still processed
// The default (nothing at all specified) is for 1 hour so we do not read the whole database
if ( !isset($_REQUEST['minTime']) && !isset($_REQUEST['maxTime']) ) {
$time = time();
$maxTime = date('c', $time);
$minTime = date('c', $time - 3600);
}
if ( isset($_REQUEST['minTime']) )
$minTime = validHtmlStr($_REQUEST['minTime']);
if ( isset($_REQUEST['maxTime']) )
$maxTime = validHtmlStr($_REQUEST['maxTime']);
// AS a special case a "all" is passed in as an extreme interval - if so, clear them here and let the database query find them
if ( (strtotime($maxTime) - strtotime($minTime))/(365*24*3600) > 30 ) {
// test years
$minTime = null;
$maxTime = null;
}
$fitMode = 1;
if ( isset($_REQUEST['fit']) && ($_REQUEST['fit'] == '0') )
@ -208,8 +222,6 @@ for ( $i = 0; $i < count($speeds); $i++ ) {
}
}
if ( isset($_REQUEST['current']) )
$defaultCurrentTime = validHtmlStr($_REQUEST['current']);
$liveMode = 1; // default to live
if ( isset($_REQUEST['live']) && ($_REQUEST['live'] == '0') )

View File

@ -45,6 +45,7 @@ $tabs['lowband'] = translate('LowBW');
$tabs['users'] = translate('Users');
$tabs['control'] = translate('Control');
$tabs['privacy'] = translate('Privacy');
$tabs['MQTT'] = translate('MQTT');
$tab = isset($_REQUEST['tab']) ? validHtmlStr($_REQUEST['tab']) : 'system';
@ -398,7 +399,6 @@ foreach (array_map('basename', glob('skins/'.$skin.'/css/*', GLOB_ONLYDIR)) as $
} // $tab == API
else {
$config = array();
$configCat = array();
$configCats = array();
$result = $dbConn->query('SELECT * FROM `Config` ORDER BY `Id` ASC');
@ -407,11 +407,10 @@ foreach (array_map('basename', glob('skins/'.$skin.'/css/*', GLOB_ONLYDIR)) as $
} else {
while ($row = dbFetchNext($result)) {
$config[$row['Name']] = $row;
if (!($configCat = &$configCats[$row['Category']])) {
if ( !($configCat = &$configCats[$row['Category']]) ) {
$configCats[$row['Category']] = array();
$configCat = &$configCats[$row['Category']];
}
$configCat[$row['Name']] = $row;
$configCats[$row['Category']][$row['Name']] = &$config[$row['Name']];
}
}
@ -434,7 +433,6 @@ foreach (array_map('basename', glob('skins/'.$skin.'/css/*', GLOB_ONLYDIR)) as $
$offsets[] = $offset = $now->getOffset();
$timezones[$timezone] = '(' . format_GMT_offset($offset) . ') ' . format_timezone_name($timezone);
}
array_multisort($offsets, $timezones);
}
@ -458,98 +456,89 @@ foreach (array_map('basename', glob('skins/'.$skin.'/css/*', GLOB_ONLYDIR)) as $
$configCats[$tab]['ZM_LOCALE_DEFAULT']['Hint'] = array(''=> translate('System Default')) + ResourceBundle::getLocales('');
} # end if tab == system
?>
<form name="optionsForm" class="" method="post" action="?">
<input type="hidden" name="view" value="<?php echo $view ?>"/>
<input type="hidden" name="tab" value="<?php echo $tab ?>"/>
<input type="hidden" name="action" value="options"/>
<form name="optionsForm" method="post" action="?">
<input type="hidden" name="view" value="<?php echo $view ?>"/>
<input type="hidden" name="tab" value="<?php echo $tab ?>"/>
<input type="hidden" name="action" value="options"/>
<?php
$configCat = $configCats[$tab];
foreach ($configCat as $name=>$value) {
$shortName = preg_replace('/^ZM_/', '', $name);
$optionPromptText = !empty($OLANG[$shortName])?$OLANG[$shortName]['Prompt']:$value['Prompt'];
$optionCanEdit = $canEdit && !$value['System'];
if (!isset($configCats[$tab])) {
echo 'There are no config entries for category '.$tab.'.<br/>';
} else {
foreach ($configCats[$tab] as $name=>$value) {
$shortName = preg_replace( '/^ZM_/', '', $name );
$optionPromptText = !empty($OLANG[$shortName])?$OLANG[$shortName]['Prompt']:$value['Prompt'];
$optionCanEdit = $canEdit && !$value['System'];
?>
<div class="form-group form-row">
<label for="<?php echo $name ?>" class="col-md-4 control-label text-md-right"><?php echo $shortName ?></label>
<div class="col-md">
<?php
if ($value['Type'] == 'boolean') {
echo '<input type="checkbox" id="'.$name.'" name="newConfig['.$name.']" value="1"'.
( $value['Value'] ? ' checked="checked"' : '').
( $optionCanEdit ? '' : ' disabled="disabled"').' />'.PHP_EOL;
} else if (is_array($value['Hint'])) {
echo htmlSelect("newConfig[$name]", $value['Hint'], $value['Value']);
} else if (preg_match('/\|/', $value['Hint'])) {
$options = explode('|', $value['Hint']);
if (count($options) > 3) {
if ($value['Type'] == 'boolean') {
echo '<input type="checkbox" id="'.$name.'" name="newConfig['.$name.']" value="1"'.
( $value['Value'] ? ' checked="checked"' : '').
( $optionCanEdit ? '' : ' disabled="disabled"').' />'.PHP_EOL;
} else if (is_array($value['Hint'])) {
echo htmlSelect("newConfig[$name]", $value['Hint'], $value['Value']);
} else if (preg_match('/\|/', $value['Hint'])) {
$options = explode('|', $value['Hint']);
if (count($options) > 3) {
$html_options = array();
foreach ($options as $option) {
if (preg_match('/^([^=]+)=(.+)$/', $option, $matches)) {
$html_options[$matches[2]] = $matches[1];
} else {
$html_options[$option] = $option;
}
}
echo htmlSelect("newConfig[$name]", $html_options, $value['Value'],
$optionCanEdit?array('class'=>'form-control-sm') : array('class'=>'form-control-sm', 'disabled'=>'disabled'));
} else {
foreach ($options as $option) {
if (preg_match('/^([^=]+)=(.+)$/', $option)) {
$optionLabel = $matches[1];
$optionValue = $matches[2];
} else {
$optionLabel = $optionValue = $option;
}
?>
<select class="form-control-sm" name="newConfig[<?php echo $name ?>]"<?php echo $optionCanEdit?'':' disabled="disabled"' ?>>
<?php
foreach ($options as $option) {
if (preg_match('/^([^=]+)=(.+)$/', $option, $matches)) {
$optionLabel = $matches[1];
$optionValue = $matches[2];
} else {
$optionLabel = $optionValue = $option;
}
echo '<option value="'.$optionValue.'"'.(($value['Value'] == $optionValue) ? ' selected="selected"' : '').'>'.htmlspecialchars($optionLabel).'</option>'.PHP_EOL;
}
?>
</select>
<?php
} else {
foreach ($options as $option) {
if (preg_match('/^([^=]+)=(.+)$/', $option)) {
$optionLabel = $matches[1];
$optionValue = $matches[2];
} else {
$optionLabel = $optionValue = $option;
}
?>
<label class="font-weight-bold form-control-sm">
<label class="font-weight-bold form-control-sm">
<input type="radio" id="<?php echo $name.'_'.preg_replace('/[^a-zA-Z0-9]/', '', $optionValue) ?>" name="newConfig[<?php echo $name ?>]" value="<?php echo $optionValue ?>"<?php if ( $value['Value'] == $optionValue ) { ?> checked="checked"<?php } ?><?php echo $optionCanEdit?'':' disabled="disabled"' ?>/>
<?php echo htmlspecialchars($optionLabel) ?>
</label>
<?php
}
} # end count(options)
} else if ($value['Type'] == 'text') {
} # end foreach option
} # end if count options > 3
} else if ( $value['Type'] == 'text' ) {
echo '<textarea class="form-control-sm" id="'.$name.'" name="newConfig['.$name.']" rows="5" cols="40"'.($optionCanEdit?'':' disabled="disabled"').'>'.validHtmlStr($value['Value']).'</textarea>'.PHP_EOL;
} else if ( $value['Type'] == 'integer' ) {
echo '<input type="number" class="form-control-sm" id="'.$name.'" name="newConfig['.$name.']" value="'.validHtmlStr($value['Value']).'" '.($optionCanEdit?'':' disabled="disabled"' ).' step="1"/>'.PHP_EOL;
} else if ( $value['Type'] == 'hexadecimal' ) {
echo '<input type="text" class="form-control-sm" id="'.$name.'" name="newConfig['.$name.']" value="'.validHtmlStr($value['Value']).'" '.($optionCanEdit?'':' disabled="disabled"' ).'/>'.PHP_EOL;
} else if ( $value['Type'] == 'decimal' ) {
echo '<input type="text" class="form-control-sm" id="'.$name.'" name="newConfig['.$name.']" value="'.validHtmlStr($value['Value']).'" '.($optionCanEdit?'':' disabled="disabled"' ).'/>'.PHP_EOL;
} else if ( $value['Type'] == 'password' ) {
echo '<input type="password" class="form-control-sm" id="'.$name.'" name="newConfig['.$name.']" value="'.validHtmlStr($value['Value']).'" '.($optionCanEdit?'':' disabled="disabled"' ).'/>'.PHP_EOL;
echo '<span class="material-icons md-18" data-on-click-this="toggle_password_visibility" data-password-input="'.$name.'">visibility</span>';
} else {
echo '<input type="text" class="form-control-sm" id="'.$name.'" name="newConfig['.$name.']" value="'.validHtmlStr($value['Value']).'" '.($optionCanEdit?'':' disabled="disabled"' ).'/>'.PHP_EOL;
}
?>
<textarea class="form-control-sm" id="<?php echo $name ?>" name="newConfig[<?php echo $name ?>]" rows="5" cols="40"<?php echo $optionCanEdit?'':' disabled="disabled"' ?>><?php echo validHtmlStr($value['Value']) ?></textarea>
<span class="form-text form-control-sm"><?php echo validHtmlStr($optionPromptText); echo makeHelpLink($name) ?></span>
</div><!-- End .col-sm-9 -->
</div><!-- End .form-group -->
<?php
} else if ($value['Type'] == 'integer') {
} # end foreach config entry in the category
} # end if category exists
?>
<input type="number" class="form-control-sm" id="<?php echo $name ?>" name="newConfig[<?php echo $name ?>]" value="<?php echo validHtmlStr($value['Value']) ?>" <?php echo $optionCanEdit?'':' disabled="disabled"' ?>/>
<?php
} else if ( $value['Type'] == 'hexadecimal' ) {
?>
<input type="text" class="form-control-sm" id="<?php echo $name ?>" name="newConfig[<?php echo $name ?>]" value="<?php echo validHtmlStr($value['Value']) ?>" <?php echo $optionCanEdit?'':' disabled="disabled"' ?>/>
<?php
} else if ($value['Type'] == 'decimal') {
?>
<input type="text" class="form-control-sm" id="<?php echo $name ?>" name="newConfig[<?php echo $name ?>]" value="<?php echo validHtmlStr($value['Value']) ?>" <?php echo $optionCanEdit?'':' disabled="disabled"' ?>/>
<?php
} else {
?>
<input type="text" class="form-control-sm" id="<?php echo $name ?>" name="newConfig[<?php echo $name ?>]" value="<?php echo validHtmlStr($value['Value']) ?>" <?php echo $optionCanEdit?'':' disabled="disabled"' ?>/>
<?php
}
?>
<span class="form-text form-control-sm"><?php echo validHtmlStr($optionPromptText); echo makeHelpLink($name) ?></span>
</div><!-- End .col-sm-9 -->
</div><!-- End .form-group -->
<?php
}
?>
<div id="contentButtons">
<button type="submit" <?php echo $canEdit?'':' disabled="disabled"' ?>><?php echo translate('Save') ?></button>
</div>
</form>
<div id="contentButtons">
<button type="submit" <?php echo $canEdit?'':' disabled="disabled"' ?>><?php echo translate('Save') ?></button>
</div>
</form>
<?php
}
?>
</div><!-- end #options -->
</div>
</div> <!-- end row -->
</div>
</div><!-- end #options -->
</div>
</div> <!-- end row -->
</div>
<?php xhtmlFooter() ?>

View File

@ -65,7 +65,7 @@ foreach ($displayMonitors as &$row) {
unset($row);
} # end foreach Monitor
if ($monitor_index == -1) {
if ($mid and ($monitor_index == -1)) {
ZM\Error("How did we not find monitor_index?");
}