Merge branch 'master' into reports
commit
e24432ee4d
|
@ -13,6 +13,10 @@ jobs:
|
|||
dist: buster
|
||||
- os: debian
|
||||
dist: bullseye
|
||||
- os: ubuntu
|
||||
dist: bionic
|
||||
- os: ubuntu
|
||||
dist: focal
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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@;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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';
|
||||
|
|
|
@ -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)
|
|
@ -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} ) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -227,9 +227,10 @@ zmDbRow::~zmDbRow() {
|
|||
}
|
||||
|
||||
zmDbQueue::zmDbQueue() :
|
||||
mThread(&zmDbQueue::process, this),
|
||||
mTerminate(false)
|
||||
{ }
|
||||
{
|
||||
mThread = std::thread(&zmDbQueue::process, this);
|
||||
}
|
||||
|
||||
zmDbQueue::~zmDbQueue() {
|
||||
stop();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 ) {
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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="
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
24
src/zmu.cpp
24
src/zmu.cpp
|
@ -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) {
|
||||
|
|
|
@ -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() : '';
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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.
Binary file not shown.
|
@ -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'])) {
|
||||
|
|
|
@ -91,6 +91,7 @@ if ($action == 'save') {
|
|||
'DecodingEnabled' => 0,
|
||||
'JanusEnabled' => 0,
|
||||
'JanusAudioEnabled' => 0,
|
||||
'Janus_Use_RTSP_Restream' => 0,
|
||||
'Exif' => 0,
|
||||
'RTSPDescribe' => 0,
|
||||
'V4LMultiBuffer' => '',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.'
|
||||
|
|
2227
web/lang/ru_ru.php
2227
web/lang/ru_ru.php
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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') ?>";
|
||||
|
|
|
@ -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¤t=<?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>
|
||||
|
|
|
@ -28,5 +28,3 @@ monitorData[monitorData.length] = {
|
|||
<?php
|
||||
} // end foreach monitor
|
||||
?>
|
||||
|
||||
var SCALE_BASE = <?php echo SCALE_BASE ?>;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -13,8 +13,6 @@
|
|||
global $popup;
|
||||
?>
|
||||
|
||||
var SCALE_BASE = <?php echo SCALE_BASE ?>;
|
||||
|
||||
//
|
||||
// PHP variables to JS
|
||||
//
|
||||
|
|
|
@ -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 ?>;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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 ?>;
|
||||
|
|
|
@ -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 ?>";
|
||||
|
|
|
@ -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 :
|
||||
|
|
|
@ -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
|
||||
//
|
||||
// ¤t = 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') )
|
||||
|
|
|
@ -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() ?>
|
||||
|
|
|
@ -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?");
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue