Merge branch 'master' of github.com:ZoneMinder/zoneminder

pull/3571/head
Isaac Connor 2022-08-09 15:48:06 -04:00
commit 165ccf5820
11 changed files with 381 additions and 44 deletions

View File

@ -667,7 +667,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@;

View File

@ -3834,6 +3834,53 @@ our @options = (
type => $types{string},
category => 'config',
},
# Add options for Alarm Server
{
name => 'ZM_OPT_USE_ALARMSERVER',
default => 'no',
description => 'Enable NETSurveillance WEB Camera ALARM SERVER',
help => q`
Alarm Server that works with cameras that use Netsurveillance Web Server,
and has the Alarm Server option it receives alarms sent by this cameras
(once enabled), and pass to Zoneminder the events.
It requires pyzm installed, visit https://pyzm.readthedocs.io/en/latest/
for installation instructions.
`,
type => $types{boolean},
category => 'system',
},
{
name => 'ZM_OPT_ALS_LOGENTRY',
default => 'no',
description => 'Makes ALARM SERVER create a log entry in ZoneMinder on Human Detected',
help => '',
type => $types{boolean},
category => 'system',
},
{
name => 'ZM_OPT_ALS_ALARM',
default => 'no',
description => 'Send the Human Detected alarm from ALARM SERVER to ZoneMinder, It does not work along with OPT_ALS_TRIGGEREVENT',
help => '',
type => $types{boolean},
category => 'system',
},
{
name => 'ZM_OPT_ALS_TRIGGEREVENT',
default => 'no',
description => 'Trigger an event on Human Detected alarm from ALARM SERVER to ZoneMinder. Requires the zmTrigger option Enabled',
help => '',
type => $types{boolean},
category => 'system',
},
{
name => 'ZM_OPT_ALS_PORT',
default => '15002',
description => 'Port Number to receive alarms from Alarm Server',
help => '',
type => $types{integer},
category => 'system',
},
);
our %options_hash = map { ( $_->{name}, $_ ) } @options;

View File

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

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

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

View File

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

View File

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

View File

@ -262,7 +262,9 @@ void PacketQueue::clearPackets(const std::shared_ptr<ZMPacket> &add_packet) {
return;
}
int keyframe_interval = 1;
int max_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");
@ -297,11 +299,13 @@ void PacketQueue::clearPackets(const std::shared_ptr<ZMPacket> &add_packet) {
if (zm_packet->packet->stream_index == video_stream_id) {
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(4, "Have a video keyframe so setting next front to it. Keyframe interval so far is %d", keyframe_interval_count);
if (max_keyframe_interval < keyframe_interval_count) max_keyframe_interval = keyframe_interval_count;
keyframe_interval_count = 1;
next_front = it;
} else {
keyframe_interval++;
keyframe_interval_count++;
}
++video_packets_to_delete;
if (packet_counts[video_stream_id] - video_packets_to_delete <= pre_event_video_packet_count + tail_count) {
@ -313,7 +317,7 @@ void PacketQueue::clearPackets(const std::shared_ptr<ZMPacket> &add_packet) {
++it;
} // end while
if ((keyframe_interval == 1) and max_video_packet_count) {
if ((max_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.");
@ -321,7 +325,7 @@ void PacketQueue::clearPackets(const std::shared_ptr<ZMPacket> &add_packet) {
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,7 +354,7 @@ 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() {

View File

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

View File

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

View File

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

View File

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