diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index e5910bddb..6404f8d26 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -781,6 +781,7 @@ INSERT INTO `Controls` VALUES (NULL,'D-LINK DCS-3415','Remote','DCS3415',0,0,0,1 INSERT INTO `Controls` VALUES (NULL,'IOS Camera','Ffmpeg','IPCAMIOS',0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'Dericam P2','Ffmpeg','DericamP2',0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,10,0,1,1,1,0,0,0,1,1,0,0,0,0,1,1,45,0,0,1,0,0,0,0,1,1,45,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'Trendnet','Remote','Trendnet',1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); +INSERT INTO `Controls` VALUES (NULL,'PSIA','Remote','PSIA',0,0,0,1,0,0,1,0,0,0,0,0,0,0,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,20,0,1,1,1,0,0,1,0,1,0,0,0,0,1,-100,100,0,0,1,0,0,0,0,1,-100,100,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'Dahua','Remote','Dahua',0,0,0,1,0,0,1,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,20,0,1,1,1,0,0,1,0,1,0,0,0,0,1,1,8,0,0,1,0,0,0,0,1,1,8,0,0,0,0); -- diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Control/PSIA.pm b/scripts/ZoneMinder/lib/ZoneMinder/Control/PSIA.pm new file mode 100644 index 000000000..fe067fe1f --- /dev/null +++ b/scripts/ZoneMinder/lib/ZoneMinder/Control/PSIA.pm @@ -0,0 +1,365 @@ +package ZoneMinder::Control::PSIA; + +use 5.006; +use strict; +use warnings; + +require ZoneMinder::Base; +require ZoneMinder::Control; + +our @ISA = qw(ZoneMinder::Control); + +our $REALM = 'TV-IP450PI'; +our $USERNAME = 'admin'; +our $PASSWORD = ''; +our $ADDRESS = ''; +our $PROTOCOL = 'http://'; + +use ZoneMinder::Logger qw(:all); +use ZoneMinder::Config qw(:all); +use ZoneMinder::Database qw(zmDbConnect); + +sub open +{ + my $self = shift; + $self->loadMonitor(); + + if ( ( $self->{Monitor}->{ControlAddress} =~ /^(?https?:\/\/)?(?[^:@]+)?:?(?[^\/@]+)?@?(?
.*)$/ ) ) { + $PROTOCOL = $+{PROTOCOL} if $+{PROTOCOL}; + $USERNAME = $+{USERNAME} if $+{USERNAME}; + $PASSWORD = $+{PASSWORD} if $+{PASSWORD}; + $ADDRESS = $+{ADDRESS} if $+{ADDRESS}; + } 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'); + $ADDRESS .= ':80'; + } + + use LWP::UserAgent; + $self->{ua} = LWP::UserAgent->new; + $self->{ua}->agent( "ZoneMinder Control Agent/".$ZoneMinder::Base::ZM_VERSION ); + $self->{state} = 'closed'; + Debug( "sendCmd credentials control address:'".$ADDRESS + ."' realm:'" . $REALM + . "' username:'" . $USERNAME + . "' password:'".$PASSWORD + ."'" + ); + $self->{ua}->credentials($ADDRESS, $REALM, $USERNAME, $PASSWORD); + + # Detect REALM + my $req = HTTP::Request->new(GET=>$PROTOCOL . $ADDRESS . "/PSIA/PTZ/channels"); + my $res = $self->{ua}->request($req); + + if ($res->is_success) { + $self->{state} = 'open'; + return; + } elsif (! $res->is_success) { + Debug("Need newer REALM"); + if ( $res->status_line() eq '401 Unauthorized' ) { + my $headers = $res->headers(); + foreach my $k ( keys %$headers ) { + Debug("Initial Header $k => $$headers{$k}"); + } # end foreach + if ( $$headers{'www-authenticate'} ) { + my ($auth, $tokens) = $$headers{'www-authenticate'} =~ /^(\w+)\s+(.*)$/; + if ($tokens =~ /\w+="([^"]+)"/i) { + $REALM = $1; + Debug("Changing REALM to $REALM"); + $self->{ua}->credentials($ADDRESS, $REALM, $USERNAME, $PASSWORD); + } # end if + } else { + Debug("No WWW-Authenticate header"); + } # end if www-authenticate header + } # end if $res->status_line() eq '401 Unauthorized' + } # end elsif ! $res->is_success +} + +sub close +{ + my $self = shift; + $self->{state} = 'closed'; +} + +sub printMsg +{ + my $self = shift; + my $msg = shift; + my $msg_len = length($msg); + + Debug( $msg."[".$msg_len."]" ); +} + +sub sendGetRequest { + my $self = shift; + my $url_path = shift; + + my $result = undef; + + my $url = $PROTOCOL . $ADDRESS . $url_path; + my $req = HTTP::Request->new(GET=>$url); + + my $res = $self->{ua}->request($req); + + if ($res->is_success) { + $result = !undef; + } else { + if ( $res->status_line() eq '401 Unauthorized' ) { + Error( "Error check failed, trying again: USERNAME: $USERNAME realm: $REALM password: " . $PASSWORD ); + Error("Content was " . $res->content() ); + my $res = $self->{ua}->request($req); + if ( $res->is_success ) { + $result = !undef; + } else { + Error("Content was " . $res->content() ); + } + } + if ( ! $result ) { + Error("Error check failed: '".$res->status_line()); + } + } + return($result); +} +sub sendPutRequest { + my $self = shift; + my $url_path = shift; + my $content = shift; + + my $result = undef; + + my $url = $PROTOCOL . $ADDRESS . $url_path; + my $req = HTTP::Request->new(PUT=>$url); + if(defined($content)) { + $req->content_type("application/x-www-form-urlencoded; charset=UTF-8"); + $req->content('' . "\n" . $content); + } + + my $res = $self->{ua}->request($req); + + if ($res->is_success) { + $result = !undef; + } else { + if ( $res->status_line() eq '401 Unauthorized' ) { + Error( "Error check failed, trying again: USERNAME: $USERNAME realm: $REALM password: " . $PASSWORD ); + Error("Content was " . $res->content() ); + my $res = $self->{ua}->request($req); + if ( $res->is_success ) { + $result = !undef; + } else { + Error("Content was " . $res->content() ); + } + } + if ( ! $result ) { + Error( "Error check failed: '".$res->status_line()."' cmd:'".$cmd."'" ); + } + } + return($result); +} +sub sendDeleteRequest { + my $self = shift; + my $url_path = shift; + + my $result = undef; + + my $url = $PROTOCOL . $ADDRESS . $url_path; + my $req = HTTP::Request->new(DELETE=>$url); + my $res = $self->{ua}->request($req); + if ($res->is_success) { + $result = !undef; + } else { + if ( $res->status_line() eq '401 Unauthorized' ) { + Error( "Error check failed, trying again: USERNAME: $USERNAME realm: $REALM password: " . $PASSWORD ); + Error("Content was " . $res->content() ); + my $res = $self->{ua}->request($req); + if ( $res->is_success ) { + $result = !undef; + } else { + Error("Content was " . $res->content() ); + } + } + if ( ! $result ) { + Error( "Error check failed: '".$res->status_line()."' cmd:'".$cmd."'" ); + } + } + return($result); +} + +sub move +{ + my $self = shift; + my $panPercentage = shift; + my $tiltPercentage = shift; + my $zoomPercentage = shift; + + my $cmd = "set_relative_pos&posX=$panSteps&posY=$tiltSteps"; + my $ptzdata = ''; + $ptzdata .= '' . $panPercentage . ''; + $ptzdata .= '' . $tiltPercentage . ''; + $ptzdata .= '' . $zoomPercentage . ''; + $ptzdata .= '500'; + $ptzdata .= ''; + $self->sendPutRequest("/PSIA/PTZ/channels/1/momentary", $ptzdata); +} + +sub moveRelUpLeft +{ + my $self = shift; + Debug( "Move Up Left" ); + $self->move(-50, 50, 0); +} + +sub moveRelUp +{ + my $self = shift; + Debug( "Move Up" ); + $self->move(0, 50, 0); +} + +sub moveRelUpRight +{ + my $self = shift; + Debug( "Move Up Right" ); + $self->move(50, 50, 0); +} + +sub moveRelLeft +{ + my $self = shift; + Debug( "Move Left" ); + $self->move(-50, 0, 0); +} + +sub moveRelRight +{ + my $self = shift; + Debug( "Move Right" ); + $self->move(50, 0, 0); +} + +sub moveRelDownLeft +{ + my $self = shift; + Debug( "Move Down Left" ); + $self->move(-50, -50, 0); +} + +sub moveRelDown +{ + my $self = shift; + Debug( "Move Down" ); + $self->move(0, -50, 0); +} + +sub moveRelDownRight +{ + my $self = shift; + Debug( "Move Down Right" ); + $self->move(50, -50, 0); +} + +sub zoomRelTele +{ + my $self = shift; + Debug("Zoom Relative Tele"); + $self->move(0, 0, 50); +} + +sub zoomRelWide +{ + my $self = shift; + Debug("Zoom Relative Wide"); + $self->move(0, 0, -50); +} + + +sub presetClear +{ + my $self = shift; + my $params = shift; + my $preset_id = $self->getParam($params, 'preset'); + my $url_path = "/PSIA/PTZ/channels/1/presets/" . $preset_id; + $self->sendDeleteRequest($url_path); +} + + +sub presetSet +{ + my $self = shift; + my $params = shift; + + my $preset_id = $self->getParam($params, 'preset'); + + my $dbh = zmDbConnect(1); + my $sql = 'SELECT * FROM ControlPresets WHERE MonitorId = ? AND Preset = ?'; + my $sth = $dbh->prepare($sql) + or Fatal("Can't prepare sql '$sql': " . $dbh->errstr()); + my $res = $sth->execute($self->{Monitor}->{Id}, $preset_id) + or Fatal("Can't execute sql '$sql': " . $sth->errstr()); + my $control_preset_row = $sth->fetchrow_hashref(); + my $new_label_name = $control_preset_row->{'Label'}; + + my $url_path = "/PSIA/PTZ/channels/1/presets/" . $preset_id; + my $ptz_preset_data = ''; + $ptz_preset_data .= '' . $preset_id . ''; + $ptz_preset_data .= '' . $new_label_name . ''; + $ptz_preset_data .= ''; + $self->sendPutRequest($url_path, $ptz_preset_data); +} + +sub presetGoto +{ + my $self = shift; + my $params = shift; + my $preset_id = $self->getParam($params, 'preset'); + + my $url_path = '/PSIA/PTZ/channels/1/presets/' . $preset_id . '/goto'; + + $self->sendPutRequest($url_path); +} + + +1; +__END__ + +=head1 NAME + +ZoneMinder::Control::PSIA - Perl module for cameras implementing the PSIA +(Physical Security Interoperability Alliance), IP Media Devices API +specification + +=head1 SYNOPSIS + +use ZoneMinder::Control::PSIA; +place this in /usr/share/perl5/ZoneMinder/Control + +=head1 DESCRIPTION + +This has so far been tested with: +- Trendnet TV-IP450PI + +=head2 EXPORT + +None by default. + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2018 ZoneMinder LLC + +This library 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 library 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. + +=cut diff --git a/src/zm_eventstream.cpp b/src/zm_eventstream.cpp index 416251089..9f7eae4ec 100644 --- a/src/zm_eventstream.cpp +++ b/src/zm_eventstream.cpp @@ -755,6 +755,12 @@ void EventStream::runStream() { // commands may set send_frame to true while(checkCommandQueue()); + // Update modified time of the socket .lock file so that we can tell which ones are stale. + if ( now.tv_sec - last_comm_update.tv_sec > 3600 ) { + touch(sock_path_lock); + last_comm_update = now; + } + if ( step != 0 ) curr_frame_id += step; diff --git a/src/zm_monitorstream.cpp b/src/zm_monitorstream.cpp index 56396e256..65e9af579 100644 --- a/src/zm_monitorstream.cpp +++ b/src/zm_monitorstream.cpp @@ -531,6 +531,12 @@ void MonitorStream::runStream() { Debug(2, "Have checking command Queue for connkey: %d", connkey ); got_command = true; } + // Update modified time of the socket .lock file so that we can tell which ones are stale. + if ( now.tv_sec - last_comm_update.tv_sec > 3600 ) { + touch(sock_path_lock); + last_comm_update = now; + } + } if ( paused ) { diff --git a/src/zm_stream.cpp b/src/zm_stream.cpp index c9f74622a..7ab844d78 100644 --- a/src/zm_stream.cpp +++ b/src/zm_stream.cpp @@ -332,6 +332,8 @@ void StreamBase::openComms() { snprintf(rem_sock_path, sizeof(rem_sock_path), "%s/zms-%06dw.sock", staticConfig.PATH_SOCKS.c_str(), connkey); strncpy(rem_addr.sun_path, rem_sock_path, sizeof(rem_addr.sun_path)-1); rem_addr.sun_family = AF_UNIX; + + gettimeofday(&last_comm_update, NULL); } // end if connKey > 0 Debug(2, "comms open"); } // end void StreamBase::openComms() diff --git a/src/zm_stream.h b/src/zm_stream.h index 4e8343b8c..f9364bbef 100644 --- a/src/zm_stream.h +++ b/src/zm_stream.h @@ -85,6 +85,7 @@ protected: int step; struct timeval now; + struct timeval last_comm_update; double base_fps; double effective_fps; diff --git a/src/zm_utils.cpp b/src/zm_utils.cpp index 94c681dd5..adc10cf8e 100644 --- a/src/zm_utils.cpp +++ b/src/zm_utils.cpp @@ -24,6 +24,8 @@ #include #include #include +#include /* Definition of AT_* constants */ +#include #if defined(__arm__) #include #endif @@ -414,3 +416,22 @@ Warning("ZM Compiled without LIBCURL. UriDecoding not implemented."); #endif } +void touch(const char *pathname) { + int fd = open(pathname, + O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK, + 0666); + if ( fd < 0 ) { + // Couldn't open that path. + Error("Couldn't open() path \"%s in touch", pathname); + return; + } + int rc = utimensat(AT_FDCWD, + pathname, + nullptr, + 0); + if ( rc ) { + Error("Couldn't utimensat() path %s in touch", pathname); + return; + } +} + diff --git a/src/zm_utils.h b/src/zm_utils.h index 8352edecb..d1340cf4b 100644 --- a/src/zm_utils.h +++ b/src/zm_utils.h @@ -63,5 +63,5 @@ extern unsigned int neonversion; char *timeval_to_string( struct timeval tv ); std::string UriDecode( const std::string &encoded ); - +void touch( const char *pathname ); #endif // ZM_UTILS_H diff --git a/web/views/image.php b/web/views/image.php index 552b81256..48e005c70 100644 --- a/web/views/image.php +++ b/web/views/image.php @@ -76,6 +76,23 @@ if ( empty($_REQUEST['path']) ) { return; } + # if alarm, get the fid of the first alarmed frame if available and let the + # fid= code continue processing it. Sort it to get the first alarmed frame + if ( $_REQUEST['fid'] == 'alarm' ) { + $Frame = Frame::find_one(array('EventId'=>$_REQUEST['eid'], 'Type'=>'Alarm'), + array('order'=>'FrameId ASC')); + if ( !$Frame ) # no alarms + $Frame = Frame::find_one(array('EventId'=>$_REQUEST['eid'])); # first frame + if ( !$Frame ) { + Warning("No frame found for event " + $_REQUEST['eid']); + $Frame = new Frame(); + $Frame->Delta(1); + $Frame->FrameId('snapshot'); + } + $_REQUEST['fid']=$Frame->FrameId(); + } + + if ( $_REQUEST['fid'] == 'snapshot' ) { $Frame = Frame::find_one(array('EventId'=>$_REQUEST['eid'], 'Score'=>$Event->MaxScore())); if ( !$Frame )