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

pull/3825/merge
Isaac Connor 2024-11-08 18:07:08 -05:00
commit 292f0d9611
18 changed files with 254 additions and 229 deletions

View File

@ -18,7 +18,7 @@
%global zmtargetdistro %{?rhel:el%{rhel}}%{!?rhel:fc%{fedora}} %global zmtargetdistro %{?rhel:el%{rhel}}%{!?rhel:fc%{fedora}}
Name: zoneminder Name: zoneminder
Version: 1.37.64 Version: 1.37.65
Release: 1%{?dist} Release: 1%{?dist}
Summary: A camera monitoring and analysis tool Summary: A camera monitoring and analysis tool
Group: System Environment/Daemons Group: System Environment/Daemons

View File

@ -39,7 +39,7 @@ our @ISA = qw(ZoneMinder::Control);
# #
# Set the following: # Set the following:
# ControlAddress: username:password@camera_webaddress:port # ControlAddress: username:password@camera_webaddress:port
# ControlDevice: IP Camera Model # ControlDevice: IP Camera Model or Device 1
# #
# ========================================================================== # ==========================================================================
@ -51,27 +51,30 @@ use LWP::UserAgent;
use HTTP::Cookies; use HTTP::Cookies;
use URI; use URI;
use URI::Encode qw(uri_encode); use URI::Encode qw(uri_encode);
use Data::Dumper;
#use Crypt::Mode::CBC;
#use Crypt::Cipher::AES;
my $ChannelID = 1; # Usually... my $ChannelID = 1; # Usually...
my $DefaultFocusSpeed = 50; # Should be between 1 and 100 my $DefaultFocusSpeed = 50; # Should be between 1 and 100
my $DefaultIrisSpeed = 50; # Should be between 1 and 100 my $DefaultIrisSpeed = 50; # Should be between 1 and 100
my $uri; my $uri;
my ($user, $pass, $host, $port, $realm) = ();
sub credentials { sub credentials {
my $self = shift; my $self = shift;
($user, $pass) = @_; $$self{username} = shift;
Debug("Setting credentials to $user/$pass"); $$self{password} = shift;
Debug("Setting credentials to $$self{username}/$$self{password}");
} }
sub open { sub open {
my $self = shift; my $self = shift;
$self->loadMonitor(); $self->loadMonitor();
$port = 80; $$self{port} = 80;
# Create a UserAgent for the requests # Create a UserAgent for the requests
$self->{UA} = LWP::UserAgent->new(); $self->{ua} = LWP::UserAgent->new();
$self->{UA}->cookie_jar( {} ); $self->{ua}->cookie_jar( {} );
# Extract the username/password host/port from ControlAddress # Extract the username/password host/port from ControlAddress
if ($self->{Monitor}{ControlAddress} if ($self->{Monitor}{ControlAddress}
@ -81,107 +84,68 @@ sub open {
$self->{Monitor}{ControlAddress} ne 'user:port@ip' $self->{Monitor}{ControlAddress} ne 'user:port@ip'
) { ) {
Debug("Using ControlAddress for credentials: $self->{Monitor}{ControlAddress}"); Debug("Using ControlAddress for credentials: $self->{Monitor}{ControlAddress}");
if ($self->{Monitor}{ControlAddress} =~ /^([^:]+):([^@]+)@(.+)/ ) { # user:pass@host... $uri = URI->new($self->{Monitor}->{ControlAddress});
$user = $1 if !$user; $uri = URI->new('http://'.$self->{Monitor}->{ControlAddress}) if ref($uri) eq 'URI::_foreign';
$pass = $2 if !$pass; $$self{host} = $uri->host();
$host = $3; if ( $uri->userinfo()) {
} elsif ( $self->{Monitor}{ControlAddress} =~ /^([^@]+)@(.+)/ ) { # user@host... @$self{'username','password'} = $uri->userinfo() =~ /^(.*):(.*)$/;
$user = $1 if !$user; } else {
$host = $2; $$self{username} = $self->{Monitor}->{User};
} else { # Just a host $$self{password} = $self->{Monitor}->{Pass};
$host = $self->{Monitor}{ControlAddress};
} }
# Check if it is a host and port or just a host # Check if it is a host and port or just a host
if ( $host =~ /([^:]+):(.+)/ ) { if ( $$self{host} =~ /([^:]+):(.+)/ ) {
$host = $1; $$self{host} = $1;
$port = $2 ? $2 : $port; $$self{port} = $2 ? $2 : $$self{port};
} }
} elsif ($self->{Monitor}{Path}) { } elsif ($self->{Monitor}{Path}) {
Debug("Using Path for credentials: $self->{Monitor}{Path}"); Debug("Using Path for credentials: $self->{Monitor}{Path}");
if (($self->{Monitor}->{Path} =~ /^(?<PROTOCOL>(https?|rtsp):\/\/)?(?<USERNAME>[^:@]+)?:?(?<PASSWORD>[^\/@]+)?@(?<ADDRESS>[^:\/]+)/)) { if (($self->{Monitor}->{Path} =~ /^(?<PROTOCOL>(https?|rtsp):\/\/)?(?<USERNAME>[^:@]+)?:?(?<PASSWORD>[^\/@]+)?@(?<ADDRESS>[^:\/]+)/)) {
Debug("Have " . $+{USERNAME}); Debug("Have " . $+{USERNAME});
Debug("Have " . $+{PASSWORD}); Debug("Have " . $+{PASSWORD});
$user = $+{USERNAME} if $+{USERNAME} and !$user; $$self{username} = $+{USERNAME} if $+{USERNAME} and !$$self{username};
$pass = $+{PASSWORD} if $+{PASSWORD} and !$pass; $$self{password} = $+{PASSWORD} if $+{PASSWORD} and !$$self{password};
$host = $+{ADDRESS} if $+{ADDRESS}; $$self{host} = $+{ADDRESS} if $+{ADDRESS};
} elsif (($self->{Monitor}->{Path} =~ /^(?<PROTOCOL>(https?|rtsp):\/\/)?(?<ADDRESS>[^:\/]+)/)) { } elsif (($self->{Monitor}->{Path} =~ /^(?<PROTOCOL>(https?|rtsp):\/\/)?(?<ADDRESS>[^:\/]+)/)) {
$host = $+{ADDRESS} if $+{ADDRESS}; $$self{host} = $+{ADDRESS} if $+{ADDRESS};
$user = $self->{Monitor}->{User} if $self->{Monitor}->{User} and !$user; $$self{username} = $self->{Monitor}->{User} if $self->{Monitor}->{User} and !$$self{username};
$pass = $self->{Monitor}->{Pass} if $self->{Monitor}->{Pass} and !$pass; $$self{password} = $self->{Monitor}->{Pass} if $self->{Monitor}->{Pass} and !$$self{password};
} else { } else {
$user = $self->{Monitor}->{User} if $self->{Monitor}->{User} and !$user; $$self{username}= $self->{Monitor}->{User} if $self->{Monitor}->{User} and !$$self{username};
$pass = $self->{Monitor}->{Pass} if $self->{Monitor}->{Pass} and !$pass; $$self{password} = $self->{Monitor}->{Pass} if $self->{Monitor}->{Pass} and !$$self{password};
} }
$uri = URI->new($self->{Monitor}->{Path}); $uri = URI->new($self->{Monitor}->{Path});
$uri->scheme('http'); $uri->scheme('http');
$uri->port(80); $uri->port(80);
$uri->path(''); $uri->path('');
$host = $uri->host(); $$self{host} = $uri->host();
} else { } else {
Debug('Not using credentials'); Debug('Not using credentials');
} }
# Save the base url # Save the base url
$self->{BaseURL} = "http://$host:$port"; $self->{BaseURL} = "http://$$self{host}:$$self{port}";
$ChannelID = $self->{Monitor}{ControlDevice} if $self->{Monitor}{ControlDevice} =~ /^\d+$/;
$$self{realm} = defined($self->{Monitor}->{ControlDevice}) ? $self->{Monitor}->{ControlDevice} : '';
$ChannelID = $self->{Monitor}{ControlDevice} if $self->{Monitor}{ControlDevice}; # Save and test the credentials
$realm = ''; if (defined($$self{username})) {
Debug("Credentials: $$self{host}:$$self{port}, realm:$$self{realm}, $$self{username}, $$self{password}");
if (defined($user)) { $self->{ua}->credentials("$$self{host}:$$self{port}", $$self{realm}, $$self{username}, $$self{password});
Debug("Credentials: $host:$port, realm:$realm, $user, $pass");
$self->{UA}->credentials("$host:$port", $realm, $user, $pass);
} # end if defined user } # end if defined user
my $url = $self->{BaseURL} .'/ISAPI/Streaming/channels/101'; my $url = '/ISAPI/System/deviceInfo';
my $response = $self->get($url); if ($self->get_realm($url)) {
if ($response->status_line() eq '401 Unauthorized' and defined $user) {
my $headers = $response->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}");
}
if ( $$headers{'www-authenticate'} ) {
foreach my $auth_header ( ref $$headers{'www-authenticate'} eq 'ARRAY' ? @{$$headers{'www-authenticate'}} : ($$headers{'www-authenticate'})) {
my ( $auth, $tokens ) = $auth_header =~ /^(\w+)\s+(.*)$/;
Debug("Have tokens $auth $tokens");
my %tokens = map { /(\w+)="?([^"]+)"?/i } split(', ', $tokens );
if ( $tokens{realm} ) {
if ( $realm ne $tokens{realm} ) {
$realm = $tokens{realm};
Debug("Changing REALM to $realm");
$self->{UA}->credentials("$host:$port", $realm, $user, $pass);
$response = $self->{UA}->get($url);
if ( !$response->is_success() ) {
Debug('Authentication still failed after updating REALM' . $response->status_line);
$headers = $response->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}\n");
} # end foreach
} else {
last;
}
} else {
Error('Authentication failed, not a REALM problem');
}
} else {
Debug('Failed to match realm in tokens');
} # end if
} # end foreach auth header
} else {
debug('No headers line');
} # end if headers
} # end if not authen
if ($response->is_success()) {
$self->{state} = 'open'; $self->{state} = 'open';
return !undef;
} }
Debug('Response: '. $response->status_line . ' ' . $response->content); return undef;
return $response->is_success;
} # end sub open } # end sub open
sub get { sub get {
my $self = shift; my $self = shift;
my $url = shift; my $url = $self->{BaseURL}.shift;
Debug("Getting $url"); Debug("Getting $url");
my $response = $self->{UA}->get($url); my $response = $self->{ua}->get($url);
#Debug('Response: '. $response->status_line . ' ' . $response->content); #Debug('Response: '. $response->status_line . ' ' . $response->content);
return $response; return $response;
} }
@ -191,74 +155,52 @@ sub PutCmd {
my $cmd = shift; my $cmd = shift;
my $content = shift; my $content = shift;
if (!$cmd) { if (!$cmd) {
Error("No cmd specified in PutCmd"); Error('No cmd specified in PutCmd');
return; return;
} }
Debug("Put: $cmd to ".$self->{BaseURL}.(defined($content)?' content:'.$content:''));
my $req = HTTP::Request->new(PUT => $self->{BaseURL}.'/'.$cmd); my $req = HTTP::Request->new(PUT => $self->{BaseURL}.'/'.$cmd);
if ( defined($content) ) { if ( defined($content) ) {
$req->content_type('application/x-www-form-urlencoded; charset=UTF-8'); $req->content_type('application/x-www-form-urlencoded; charset=UTF-8');
$req->content('<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $content); $req->content('<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $content);
} }
my $res = $self->{UA}->request($req); my $res = $self->{ua}->request($req);
if (!$res->is_success) { if (!$res->is_success) {
#
# The camera timeouts connections at short intervals. When this # The camera timeouts connections at short intervals. When this
# happens the user agent connects again and uses the same auth tokens. # happens the user agent connects again and uses the same auth tokens.
# The camera rejects this and asks for another token but the UserAgent # The camera rejects this and asks for another token but the UserAgent
# just gives up. Because of this I try the request again and it should # just gives up. Because of this I try the request again and it should
# succeed the second time if the credentials are correct. # succeed the second time if the credentials are correct.
# #
# Apparently it is necessary to create a new ua
if ( $res->code == 401 ) { if ( $res->code == 401 ) {
# $self->{ua} = LWP::UserAgent->new();
# It has failed authentication. The odds are $self->{ua}->cookie_jar( {} );
# that the user has set some parameter incorrectly $self->{ua}->credentials("$$self{host}:$$self{port}", $$self{realm}, $$self{username}, $$self{password});
# so check the realm against the ControlDevice
# entry and send a message if different
#
my $headers = $res->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}");
}
if ( $$headers{'www-authenticate'} ) { $res = $self->{ua}->request($req);
foreach my $auth ( ref $$headers{'www-authenticate'} eq 'ARRAY' ? @{$$headers{'www-authenticate'}} : ($$headers{'www-authenticate'})) { if (!$res->is_success) {
foreach (split(/\s*,\s*/, $auth)) { # Check for username/password
if ( $_ =~ /^realm\s*=\s*"([^"]+)"/i ) { if ( $self->{Monitor}{ControlAddress} =~ /.+:(.+)@.+/ ) {
if ($realm ne $1) { Info('Check username/password is correct');
$realm = $1; } elsif ( $self->{Monitor}{ControlAddress} =~ /^[^:]+@.+/ ) {
$self->{UA}->credentials("$host:$port", $realm, $user, $pass); Info('No password in Control Address. Should there be one?');
return PutCmd($self, $cmd, $content); } elsif ( $self->{Monitor}{ControlAddress} =~ /^:.+@.+/ ) {
} Info('Password but no username in Control Address.');
} else { } else {
Debug('Not realm: '.$_); Info('Missing username and password in Control Address.');
} }
} # end foreach auth token Error($res->status_line);
} # end foreach auth token
} else {
Debug('No authenticate header');
} }
#
# Check for username/password
#
if ( $self->{Monitor}{ControlAddress} =~ /.+:.+@.+/ ) {
Info('Check username/password is correct');
} elsif ( $self->{Monitor}{ControlAddress} =~ /^[^:]+@.+/ ) {
Info('No password in Control Address. Should there be one?');
} elsif ( $self->{Monitor}{ControlAddress} =~ /^:.+@.+/ ) {
Info('Password but no username in Control Address.');
} else {
Info('Missing username and password in Control Address.');
}
Error($res->status_line);
} else { } else {
Error($res->status_line); Error($res->status_line);
} }
} else { } else {
Debug('Success: ' . $res->content); Debug("Success sending $cmd: ".$res->content);
} # end unless res->is_success } # end unless res->is_success
Debug($res->content);
} # end sub putCmd } # end sub putCmd
#
# The move continuous functions all call moveVector # The move continuous functions all call moveVector
# with the direction to move in. This includes zoom # with the direction to move in. This includes zoom
# #
@ -270,15 +212,10 @@ sub moveVector {
my $params = shift; my $params = shift;
my $command; # The ISAPI/PTZ command my $command; # The ISAPI/PTZ command
# Calculate autostop time my $duration = $self->duration();
my $autostop = $self->getParam($params, 'autostop', 0);
my $duration = $autostop * $self->{Monitor}{AutoStopTimeout};
$duration = ($duration < 1000) ? $duration * 1000 : int($duration/1000);
# Change from microseconds to milliseconds or seconds to milliseconds
Debug("Calculate duration $duration from autostop($autostop) and AutoStopTimeout ".$self->{Monitor}{AutoStopTimeout});
my $momentxml; my $momentxml;
if ($duration) { if( $duration ) {
$momentxml = "<Momentary><duration>$duration</duration></Momentary>"; $momentxml = "<Momentary><duration>$duration</duration></Momentary>";
$command = "ISAPI/PTZCtrl/channels/$ChannelID/momentary"; $command = "ISAPI/PTZCtrl/channels/$ChannelID/momentary";
} else { } else {
@ -298,7 +235,6 @@ sub moveVector {
# Send it to the camera # Send it to the camera
$self->PutCmd($command, $xml); $self->PutCmd($command, $xml);
} }
sub zoomStop { $_[0]->moveVector( 0, 0, 0, splice(@_,1)); } sub zoomStop { $_[0]->moveVector( 0, 0, 0, splice(@_,1)); }
sub moveStop { $_[0]->moveVector( 0, 0, 0, splice(@_,1)); } sub moveStop { $_[0]->moveVector( 0, 0, 0, splice(@_,1)); }
sub moveConUp { $_[0]->moveVector( 0, 1, 0, splice(@_,1)); } sub moveConUp { $_[0]->moveVector( 0, 1, 0, splice(@_,1)); }
@ -332,6 +268,17 @@ sub presetHome {
my $params = shift; my $params = shift;
$self->PutCmd("ISAPI/PTZCtrl/channels/$ChannelID/homeposition/goto"); $self->PutCmd("ISAPI/PTZCtrl/channels/$ChannelID/homeposition/goto");
} }
sub duration() {
my $self = shift;
my $params = shift;
my $autostop = $self->getParam($params, 'autostop', 0);
my $duration = $autostop * $self->{Monitor}{AutoStopTimeout};
$duration = ($duration < 1000) ? $duration * 1000 : int($duration/1000);
# Change from microseconds to milliseconds or seconds to milliseconds
Debug("Calculate duration $duration from autostop($autostop) and AutoStopTimeout ".$self->{Monitor}{AutoStopTimeout});
return $duration;
}
# #
# Focus controls all call Focus with a +/- speed # Focus controls all call Focus with a +/- speed
# #
@ -345,12 +292,11 @@ sub focusConNear {
my $self = shift; my $self = shift;
my $params = shift; my $params = shift;
# Calculate autostop time my $duration = $self->duration();
my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
# Get the focus speed # Get the focus speed
my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed ); my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
$self->Focus(-$speed); $self->Focus(-$speed);
if($duration) { if ($duration) {
usleep($duration); usleep($duration);
$self->moveStop($params); $self->moveStop($params);
} }
@ -379,8 +325,7 @@ sub focusConFar {
my $self = shift; my $self = shift;
my $params = shift; my $params = shift;
# Calculate autostop time my $duration = $self->duration();
my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
# Get the focus speed # Get the focus speed
my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed ); my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
$self->Focus($speed); $self->Focus($speed);
@ -424,8 +369,7 @@ sub irisConClose {
my $self = shift; my $self = shift;
my $params = shift; my $params = shift;
# Calculate autostop time my $duration = $self->duration();
my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
# Get the iris speed # Get the iris speed
my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed ); my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
$self->Iris(-$speed); $self->Iris(-$speed);
@ -460,8 +404,7 @@ sub irisConOpen {
my $self = shift; my $self = shift;
my $params = shift; my $params = shift;
# Calculate autostop time my $duration = $self->duration();
my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
# Get the iris speed # Get the iris speed
my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed ); my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
$self->Iris($speed); $self->Iris($speed);
@ -502,10 +445,24 @@ sub reboot {
} }
my %config_types = ( my %config_types = (
'ISAPI/System/deviceInfo' => {
},
'ISAPI/System/time' => { 'ISAPI/System/time' => {
}, },
'ISAPI/System/time/ntpServers' => { 'ISAPI/System/time/ntpServers' => {
}, },
'ISAPI/System/Network/interfaces' => {
},
'ISAPI/System/logServer' => {
},
'ISAPI/Streaming/channels/1' => {
Video => {
videoResolutionWidth => { value=>1920 },
videoResolutionHeight => { value=>1080 },
maxFrameRate => { value=>1000}, # appears to be fps * 100
keyframeInterval => {value=>5000},
}
},
'ISAPI/Streaming/channels/101' => { 'ISAPI/Streaming/channels/101' => {
Video => { Video => {
videoResolutionWidth => { value=>1920 }, videoResolutionWidth => { value=>1920 },
@ -514,10 +471,26 @@ my %config_types = (
keyframeInterval => {value=>5000}, keyframeInterval => {value=>5000},
} }
}, },
'ISAPI/Streaming/channels/102' => {
Video => {
videoResolutionWidth => { value=>1920 },
videoResolutionHeight => { value=>1080 },
maxFrameRate => { value=>1000}, # appears to be fps * 100
keyframeInterval => {value=>5000},
}
},
'ISAPI/System/Video/inputs/channels/1/overlays' => { 'ISAPI/System/Video/inputs/channels/1/overlays' => {
}, },
'ISAPI/System/Video/inputs/channels/101/overlays' => { 'ISAPI/System/Video/inputs/channels/101/overlays' => {
}, },
'ISAPI/System/Video/inputs/channels/1/motionDetectionExt' => {
},
'ISAPI/System/Network/Integrate' => {
},
'ISAPI/Security/ONVIF/users' => {
},
'ISAPI/Security/users' => {
},
); );
sub xml_apply_updates { sub xml_apply_updates {
@ -583,7 +556,7 @@ sub get_config {
my $self = shift; my $self = shift;
my %config; my %config;
foreach my $category ( @_ ? @_ : keys %config_types ) { foreach my $category ( @_ ? @_ : keys %config_types ) {
my $response = $self->get($self->{BaseURL}.'/'.$category); my $response = $self->get('/'.$category);
Debug($response->content); Debug($response->content);
my $dom = XML::LibXML->load_xml(string => $response->content); my $dom = XML::LibXML->load_xml(string => $response->content);
if (!$dom) { if (!$dom) {
@ -609,11 +582,11 @@ sub set_config {
} }
Debug("Applying $category"); Debug("Applying $category");
my $response = $self->get($self->{BaseURL}.'/'.$category); my $response = $self->get('/'.$category);
my $dom = XML::LibXML->load_xml(string => $response->content); my $dom = XML::LibXML->load_xml(string => $response->content);
if (!$dom) { if (!$dom) {
Error('No document from :'.$response->content()); Error('No document from :'.$response->content());
return; return undef;
} }
my $xml = $dom->documentElement(); my $xml = $dom->documentElement();
xml_apply_updates($xml, $$diff{$category}); xml_apply_updates($xml, $$diff{$category});
@ -621,69 +594,49 @@ sub set_config {
Debug($xml->toString()); Debug($xml->toString());
$req->content($xml->toString()); $req->content($xml->toString());
$response = $self->{UA}->request($req); $response = $self->{ua}->request($req);
Debug( 'status:'.$response->status_line ); if (!$response->is_success()) {
Debug($response->content); Error('status:'.$response->status_line);
Debug($response->content);
return undef;
} else {
Debug('status:'.$response->status_line);
Debug($response->content);
}
} }
return !undef;
} }
sub ping { sub ping {
return -1 if ! $host; my $self = shift;
my $ip = @_ ? shift : $$self{host};
return undef if ! $ip;
require Net::Ping; require Net::Ping;
Debug("Pinging $ip");
my $p = Net::Ping->new(); my $p = Net::Ping->new();
my $rv = $p->ping($host); my $rv = $p->ping($ip);
$p->close(); $p->close();
Debug("Pinging $ip $rv");
return $rv; return $rv;
} }
sub probe { sub probe {
my ($ip, $user, $pass) = @_; my ($ip, $username, $password) = @_;
my $self = new ZoneMinder::Control::HikVision(); my $self = new ZoneMinder::Control::HikVision();
$self->set_credentials($username, $password);
# Create a UserAgent for the requests # Create a UserAgent for the requests
$self->{UA} = LWP::UserAgent->new(); $self->{ua} = LWP::UserAgent->new();
$self->{UA}->cookie_jar( {} ); $self->{ua}->cookie_jar( {} );
my $realm;
foreach my $port ( '80','443' ) { foreach ( '80','443' ) {
my $url = 'http://'.$user.':'.$pass.'@'.$ip.':'.$port.'/ISAPI/Streaming/channels/101'; $$self{port} = $_;
Debug("Probing $url"); if ($self->get_realm('/ISAPI/Streaming/channels/101')) {
my $response = $self->get($url);
if ($response->status_line() eq '401 Unauthorized' and defined $user) {
my $headers = $response->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}");
}
if ( $$headers{'www-authenticate'} ) {
my ( $auth, $tokens ) = $$headers{'www-authenticate'} =~ /^(\w+)\s+(.*)$/;
my %tokens = map { /(\w+)="?([^"]+)"?/i } split(', ', $tokens );
if ($tokens{realm}) {
$realm = $tokens{realm};
Debug('Changing REALM to '.$tokens{realm});
$self->{UA}->credentials("$ip:$port", $tokens{realm}, $user, $pass);
$response = $self->{UA}->get($url);
if (!$response->is_success()) {
Error('Authentication still failed after updating REALM' . $response->status_line);
}
$headers = $response->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}\n");
} # end foreach
} else {
Debug('Failed to match realm in tokens');
} # end if
} else {
Debug('No headers line');
} # end if headers
} # end if not authen
Debug('Response: '. $response->status_line . ' ' . $response->content);
if ($response->is_success) {
return { return {
url => 'http://'.$user.':'.$pass.'@'.$ip.':'.$port.'/h264', url => 'http://'.$$self{username}.':'.$$self{password}.'@'.$ip.':'.$$self{port}.'/h264',
realm => $realm, realm => $$self{realm},
}; };
} }
} # end foreach port } # end foreach port
@ -693,5 +646,70 @@ sub probe {
sub profiles { sub profiles {
} }
sub rtsp_url {
my ($self, $ip) = @_;
return 'rtsp://'.$ip.'/Streaming/Channels/101';
}
my %latest_firmware = (
'I918L' => {
latest_version=>'V5.7.1',
build=>20211130,
url=>'https://download.annke.com/firmware/4K_IPC/C800_5.7.1_211130.zip'
},
'DS-2CD2126G2-I' => {
'latest_version'=>'V5.7.0',
build=>240507,
url=>'https://assets.hikvision.com/prd/public/all/files/202405/1715716961127/Firmware__V5.7.0_240507_S3000573675.zip',
file=>'Firmware__V5.7.0_240507_S3000573675.zip',
},
'DS-2CD2046G2-I' => {
'latest_version'=>'V5.7.18',
build=>240826,
url=>'https://assets.hikvision.com/prd/public/all/files/202409/Firmware__V5.7.18_240826_S3000597013.zip',
file=>'Firmware__V5.7.18_240826_S3000597013.zip',
},
'DS-2CD2146G2-I' => {
'latest_version'=>'V5.7.18',
build=>240826,
url=>'https://assets.hikvision.com/prd/public/all/files/202409/Firmware__V5.7.18_240826_S3000597013.zip',
file=>'Firmware__V5.7.18_240826_S3000597013.zip',
},
'DS-2CD2142FWD-I' => {
latest_version=>'V5.5.82',
build=>190909,
file=>'IPC_R6_EN_STD_5.5.82_190909.zip',
url=>'https://www.hikvisioneurope.com/eu/portal/portal/Technical%20Materials/00%20%20Network%20Camera/00%20%20Product%20Firmware/R6%20platform%20%282X22FWD%2C%202X42FWD%2C%202X52%2C64X4FWD%2C1X31%2C1X41%29/V5.5.82_Build190909/IPC_R6_EN_STD_5.5.82_190909.zip',
},
);
sub check_firmware {
my $self = shift;
my $config = $self->get_config('ISAPI/System/deviceInfo');
print Dumper($config);
my $model = $$config{'ISAPI/System/deviceInfo'}{model};
if (!$model) {
print "No model\n";
return;
}
my $firmware = $$config{'ISAPI/System/deviceInfo'}{firmwareVersion};
if ($latest_firmware{$model}) {
my %result = %{$latest_firmware{$model}};
$result{current_version} = $firmware;
$result{current_build} = $$config{'ISAPI/System/deviceInfo'}{firmwareReleasedDate};
$result{update_available} = ($firmware lt $result{latest_version});
return %result;
} else {
Debug("We don't have a listing for latest firmware for ($model)");
}
return;
}
sub update_firmware {
my $self = shift;
my $firmware = shift;
my $response = $self->put('/ISAPI/System/updateFirmware', $firmware);
}
1; 1;
__END__ __END__

View File

@ -3396,6 +3396,7 @@ int Monitor::Pause() {
sws_freeContext(convert_context); sws_freeContext(convert_context);
convert_context = nullptr; convert_context = nullptr;
} }
decoding_image_count = 0;
} }
if (analysis_thread) { if (analysis_thread) {
Debug(1, "Joining analysis"); Debug(1, "Joining analysis");

View File

@ -741,7 +741,7 @@ class Monitor : public std::enable_shared_from_this<Monitor> {
return false; return false;
} }
if (decoding_image_count >= ready_count) { if (decoding_image_count >= ready_count) {
Debug(4, "Ready because image_count(%d) >= ready_count(%d)", decoding_image_count, ready_count); Debug(4, "Ready because decoding_image_count(%d) >= ready_count(%d)", decoding_image_count, ready_count);
return true; return true;
} }
Debug(4, "Not ready because decoding_image_count(%d) <= ready_count(%d)", decoding_image_count, ready_count); Debug(4, "Not ready because decoding_image_count(%d) <= ready_count(%d)", decoding_image_count, ready_count);

View File

@ -60,8 +60,8 @@ void MQTT::on_subscribe(int mid, int qos_count, const int *granted_qos) {
Debug(1, "MQTT: Subscribed to topic "); Debug(1, "MQTT: Subscribed to topic ");
} }
void MQTT::on_publish() { void MQTT::on_publish(int mid) {
Debug(1, "MQTT: on_publish "); Debug(1, "MQTT: on_publish %d", mid);
} }
void MQTT::send(const std::string &message) { void MQTT::send(const std::string &message) {

View File

@ -31,7 +31,7 @@ class MQTT : public mosqpp::mosquittopp {
void on_connect(int rc); void on_connect(int rc);
void on_message(const struct mosquitto_message *message); void on_message(const struct mosquitto_message *message);
void on_subscribe(int mid, int qos_count, const int *granted_qos); void on_subscribe(int mid, int qos_count, const int *granted_qos);
void on_publish(); void on_publish(int mid);
enum sensorTypes { enum sensorTypes {
NUMERIC = 0, NUMERIC = 0,
DIGITAL DIGITAL

View File

@ -1 +1 @@
1.37.64 1.37.65

View File

@ -215,14 +215,11 @@ if ( canEdit('Events') ) {
ajaxResponse(array('response'=>$response)); ajaxResponse(array('response'=>$response));
break; break;
case 'removetag' : case 'removetag' :
$tagId = $_REQUEST['tid']; $tagId = validCardinal($_REQUEST['tid']);
dbQuery('DELETE FROM Events_Tags WHERE TagId = ? AND EventId = ?', array($tagId, $_REQUEST['id'])); dbQuery('DELETE FROM Events_Tags WHERE TagId = ? AND EventId = ?', array($tagId, $_REQUEST['id']));
$sql = "SELECT * FROM Events_Tags WHERE TagId = $tagId"; $rowCount = dbNumRows('SELECT * FROM Events_Tags WHERE TagId=?', [ $tagId ]);
$rowCount = dbNumRows($sql);
if ($rowCount < 1) { if ($rowCount < 1) {
$sql = 'DELETE FROM Tags WHERE Id = ?'; $response = dbNumRows('DELETE FROM Tags WHERE Id=?', [$tagId]);
$values = array($_REQUEST['tid']);
$response = dbNumRows($sql, $values);
ajaxResponse(array('response'=>$response)); ajaxResponse(array('response'=>$response));
} }
ajaxResponse(); ajaxResponse();

View File

@ -42,8 +42,7 @@ function parentGrpSelect($newGroup) {
} }
$kids = get_children($newGroup); $kids = get_children($newGroup);
if ( $newGroup->Id() ) if ( $newGroup->Id() ) $kids[] = $newGroup->Id();
$kids[] = $newGroup->Id();
$sql = 'SELECT Id,Name FROM `Groups`'.(count($kids)?' WHERE Id NOT IN ('.implode(',',array_map(function(){return '?';}, $kids)).')' : '').' ORDER BY Name'; $sql = 'SELECT Id,Name FROM `Groups`'.(count($kids)?' WHERE Id NOT IN ('.implode(',',array_map(function(){return '?';}, $kids)).')' : '').' ORDER BY Name';
$options = array(''=>'None'); $options = array(''=>'None');
@ -57,11 +56,11 @@ function parentGrpSelect($newGroup) {
function monitorList($newGroup) { function monitorList($newGroup) {
$result = ''; $result = '';
$monitors = dbFetchAll('SELECT Id,Name FROM Monitors ORDER BY Sequence ASC'); $monitors = dbFetchAll('SELECT Id,Name FROM Monitors WHERE Deleted=false ORDER BY Sequence ASC');
$monitorIds = $newGroup->MonitorIds(); $monitorIds = $newGroup->MonitorIds();
foreach ( $monitors as $monitor ) { foreach ( $monitors as $monitor ) {
if ( visibleMonitor($monitor['Id']) ) { if ( visibleMonitor($monitor['Id']) ) {
$result .= '<option value="' .validCardinal($monitor['Id']). '"' .( in_array( $monitor['Id'], $monitorIds ) ? ' selected="selected"' : ''). '>' .validHtmlStr($monitor['Name']). '</option>'.PHP_EOL; $result .= '<option value="' .validCardinal($monitor['Id']). '"' .( in_array($monitor['Id'], $monitorIds, true) ? ' selected="selected"' : ''). '>' .validHtmlStr($monitor['Name']). '</option>'.PHP_EOL;
} }
} }
@ -112,7 +111,7 @@ if ( !empty($_REQUEST['gid']) ) {
<tr> <tr>
<th class="text-right pr-3" scope="row"><?php echo translate('Monitor') ?></th> <th class="text-right pr-3" scope="row"><?php echo translate('Monitor') ?></th>
<td> <td>
<select name="newGroup[MonitorIds][]" class="chosen" multiple="multiple" data-on-change="configModalBtns"> <select name="newGroup[MonitorIds][]" id="newGroupMonitorIds" class="chosen" multiple="multiple" data-on-change="configModalBtns">
<?php echo monitorList($newGroup) ?> <?php echo monitorList($newGroup) ?>
</select> </select>
</td> </td>

View File

@ -49,6 +49,10 @@ class Group extends ZM_Object {
public function MonitorIds( ) { public function MonitorIds( ) {
if (!property_exists($this, 'MonitorIds')) { if (!property_exists($this, 'MonitorIds')) {
if (!$this->{'Id'}) {
return $this->{'MonitorIds'} = [];
}
if (!isset($monitor_ids_cache[$this->{'Id'}])) { if (!isset($monitor_ids_cache[$this->{'Id'}])) {
$monitor_ids_cache[$this->{'Id'}] = dbFetchAll('SELECT `MonitorId` FROM `Groups_Monitors` WHERE `GroupId`=?', 'MonitorId', array($this->{'Id'})); $monitor_ids_cache[$this->{'Id'}] = dbFetchAll('SELECT `MonitorId` FROM `Groups_Monitors` WHERE `GroupId`=?', 'MonitorId', array($this->{'Id'}));
if (count($this->Children())) { if (count($this->Children())) {

View File

@ -652,7 +652,7 @@ class Monitor extends ZM_Object {
} else if ($this->ServerId()) { } else if ($this->ServerId()) {
$result = $this->Server()->SendToApi('/monitors/daemonControl/'.$this->{'Id'}.'/'.$mode.'/zmc.json'); $result = $this->Server()->SendToApi('/monitors/daemonControl/'.$this->{'Id'}.'/'.$mode.'/zmc.json');
} else { } else {
Error('Server not assigned to Monitor in a multi-server setup. Please assign a server to the Monitor.'); Error('Server not assigned to Monitor '.$this->{'Id'}.' in a multi-server setup. Please assign a server to the Monitor.');
} }
} // end function zmcControl } // end function zmcControl

View File

@ -253,6 +253,7 @@ function loadConfig( $defineConsts=true ) {
} # end function loadConfig } # end function loadConfig
require_once('Server.php'); require_once('Server.php');
global $Servers; # Ensure that it is global, api doesn't specifically make it so
$Servers = ZM\Server::find([], ['order'=>'lower(Name)']); $Servers = ZM\Server::find([], ['order'=>'lower(Name)']);
$thisServer = new ZM\Server(); $thisServer = new ZM\Server();

View File

@ -664,8 +664,6 @@ function MonitorStream(monitorData) {
} }
} }
this.buttons.forceAlarmButton.prop('disabled', false); this.buttons.forceAlarmButton.prop('disabled', false);
} else {
console.log("No forceAlarmButton");
} }
} // end if canEdit.Monitors } // end if canEdit.Monitors
@ -770,8 +768,6 @@ function MonitorStream(monitorData) {
} }
} }
this.buttons.forceAlarmButton.prop('disabled', false); this.buttons.forceAlarmButton.prop('disabled', false);
} else {
console.log("No forceAlarmButton");
} }
} else { } else {
console.log("Can't edit"); console.log("Can't edit");

View File

@ -209,6 +209,9 @@ input[disabled] {
.modal img { .modal img {
max-width: 100%; max-width: 100%;
} }
.modal table {
width: 100%;
}
img { img {
display: inline-block; display: inline-block;
} }

View File

@ -490,13 +490,11 @@ function getCmdResponse(respObj, respText) {
console.log("Stream not scaled, re-applying, current: ", currentScale + deltaScale(), " stream: ", streamStatus.scale); console.log("Stream not scaled, re-applying, current: ", currentScale + deltaScale(), " stream: ", streamStatus.scale);
streamScale(currentScale); streamScale(currentScale);
} }
console.log(streamStatus.fps);
const fps = document.getElementById('fpsValue'); const fps = document.getElementById('fpsValue');
if (fps) { if (fps) {
fps.innerHTML = streamStatus.fps; fps.innerHTML = streamStatus.fps;
} }
updateProgressBar(); updateProgressBar();
if (streamStatus.auth) { if (streamStatus.auth) {

View File

@ -1,11 +1,11 @@
// Manage the NEW Group button // Manage the NEW Group button
function newGroup() { function newGroup() {
$j('#groupModal').remove();
$j.getJSON(thisUrl + '?request=modal&modal=group') $j.getJSON(thisUrl + '?request=modal&modal=group')
.done(function(data) { .done(function(data) {
insertModalHtml('groupdModal', data.html); insertModalHtml('groupdModal', data.html);
$j('#groupModal').modal('show'); $j('#groupModal').modal('show');
$j('.chosen').chosen("destroy"); $j('#newGroupMonitorIds').chosen({width: "100%"});
$j('.chosen').chosen({width: "100%"});
}) })
.fail(logAjaxFail); .fail(logAjaxFail);
} }
@ -17,16 +17,16 @@ function setGroup( element ) {
} }
function editGroup( element ) { function editGroup( element ) {
var gid = element.getAttribute('data-group-id'); const gid = element.getAttribute('data-group-id');
if ( !gid ) { if ( !gid ) {
console.log('No group id found in editGroup'); console.log('No group id found in editGroup');
} else { } else {
$j('#groupModal').remove();
$j.getJSON(thisUrl + '?request=modal&modal=group&gid=' + gid) $j.getJSON(thisUrl + '?request=modal&modal=group&gid=' + gid)
.done(function(data) { .done(function(data) {
insertModalHtml('groupModal', data.html); insertModalHtml('groupModal', data.html);
$j('#groupModal').modal('show'); $j('#groupModal').modal('show');
$j('.chosen').chosen("destroy"); $j('#newGroupMonitorIds').chosen({width: "100%"});
$j('.chosen').chosen({width: "100%"});
}) })
.fail(logAjaxFail); .fail(logAjaxFail);
} }

View File

@ -663,9 +663,14 @@ function fullscreenchanged(event) {
objBtn.children('.material-icons').html('fullscreen'); objBtn.children('.material-icons').html('fullscreen');
} }
//Sometimes the positioning is not correct, so it is better to reset Pan & Zoom //Sometimes the positioning is not correct, so it is better to reset Pan & Zoom
zmPanZoom.panZoom[stringToNumber(event.target.id)].reset(); const monitorId = stringToNumber(event.target.id);
if (monitorId && zmPanZoom.panZoom[monitorId]) {
zmPanZoom.panZoom[monitorId].reset();
} else {
console.err("No panZoom found for ", monitorId, event);
}
} }
} } // end function fullscreenchanged(event)
function calculateAverageMonitorsRatio(arrRatioMonitors) { function calculateAverageMonitorsRatio(arrRatioMonitors) {
//Let's calculate the average Ratio value for the displayed monitors //Let's calculate the average Ratio value for the displayed monitors
@ -1180,15 +1185,17 @@ document.onvisibilitychange = () => {
}, 15*1000); }, 15*1000);
} else { } else {
TimerHideShow = clearTimeout(TimerHideShow); TimerHideShow = clearTimeout(TimerHideShow);
//Start monitors when show page if ((!ZM_WEB_VIEWING_TIMEOUT) || (idle < ZM_WEB_VIEWING_TIMEOUT)) {
for (let i = 0, length = monitors.length; i < length; i++) { //Start monitors when show page
const monitor = monitors[i]; for (let i = 0, length = monitors.length; i < length; i++) {
const monitor = monitors[i];
const isOut = isOutOfViewport(monitor.getElement()); const isOut = isOutOfViewport(monitor.getElement());
if ((!isOut.all) && !monitor.started) { if ((!isOut.all) && !monitor.started) {
monitor.start(); monitor.start();
} }
} } // end foreach monitor
} // end if not AYSW
} }
}; };

View File

@ -1431,8 +1431,9 @@ function monitorsSetScale(id=null) {
$j( window ).on("load", initPage); $j( window ).on("load", initPage);
document.onvisibilitychange = () => { document.onvisibilitychange = () => {
// Always clear it because the return to visibility might happen before timeout
TimerHideShow = clearTimeout(TimerHideShow);
if (document.visibilityState === "hidden") { if (document.visibilityState === "hidden") {
TimerHideShow = clearTimeout(TimerHideShow);
TimerHideShow = setTimeout(function() { TimerHideShow = setTimeout(function() {
//Stop monitor when closing or hiding page //Stop monitor when closing or hiding page
if (monitorStream) { if (monitorStream) {