Merge branch 'master' into storageareas
This commit is contained in:
commit
e8ed1367d7
|
@ -38,6 +38,7 @@ env:
|
||||||
- OS=el DIST=7
|
- OS=el DIST=7
|
||||||
- OS=fedora DIST=27 DOCKER_REPO=knnniggett/packpack
|
- OS=fedora DIST=27 DOCKER_REPO=knnniggett/packpack
|
||||||
- OS=fedora DIST=28 DOCKER_REPO=knnniggett/packpack
|
- OS=fedora DIST=28 DOCKER_REPO=knnniggett/packpack
|
||||||
|
- OS=fedora DIST=29 DOCKER_REPO=knnniggett/packpack
|
||||||
- OS=ubuntu DIST=trusty
|
- OS=ubuntu DIST=trusty
|
||||||
- OS=ubuntu DIST=xenial
|
- OS=ubuntu DIST=xenial
|
||||||
- OS=ubuntu DIST=trusty ARCH=i386
|
- OS=ubuntu DIST=trusty ARCH=i386
|
||||||
|
|
|
@ -53,9 +53,9 @@ Lastly, if you desire to build a development snapshot from the master branch, it
|
||||||
Please visit our [ReadtheDocs site](https://zoneminder.readthedocs.org/en/stable/installationguide/index.html) for distro specific instructions.
|
Please visit our [ReadtheDocs site](https://zoneminder.readthedocs.org/en/stable/installationguide/index.html) for distro specific instructions.
|
||||||
|
|
||||||
### Package Maintainers
|
### Package Maintainers
|
||||||
Many of the ZoneMinder configration variable default values are not configurable at build time through autotools or cmake. A new tool called *zmeditconfigdata.sh* has been added to allow package maintainers to manipulate any variable stored in ConfigData.pm without patching the source.
|
Many of the ZoneMinder configuration variable default values are not configurable at build time through autotools or cmake. A new tool called *zmeditconfigdata.sh* has been added to allow package maintainers to manipulate any variable stored in ConfigData.pm without patching the source.
|
||||||
|
|
||||||
For example, let's say I have created a new ZoneMinder package that contains the cambozola javascript file. However, by default cambozola support is turned off. To fix that, add this to the pacakging script:
|
For example, let's say I have created a new ZoneMinder package that contains the cambozola javascript file. However, by default cambozola support is turned off. To fix that, add this to the packaging script:
|
||||||
```bash
|
```bash
|
||||||
./utils/zmeditconfigdata.sh ZM_OPT_CAMBOZOLA yes
|
./utils/zmeditconfigdata.sh ZM_OPT_CAMBOZOLA yes
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,34 +1,29 @@
|
||||||
What's New
|
What's New
|
||||||
==========
|
==========
|
||||||
|
|
||||||
1. This is an *experimental* build of zoneminder which uses the
|
1. See the ZoneMinder release notes for a list of new features:
|
||||||
nginx web server.
|
https://github.com/ZoneMinder/zoneminder/releases
|
||||||
|
|
||||||
2. The Apache ScriptAlias has been changed from "/cgi-bin/zm/zms" to
|
2. The contents of the ZoneMinder Apache config file have changed. In
|
||||||
"/cgi-bin-zm/zms". This has been to done to avoid this bug:
|
addition, this ZoneMinder package now requires you to manually symlink the
|
||||||
https://bugzilla.redhat.com/show_bug.cgi?id=973067
|
ZoneMinder Apache config file. See new install step 6 and upgrade step 3
|
||||||
|
below for details.
|
||||||
|
|
||||||
IMPORTANT: You must manually inspect the value for PATH_ZMS under Options
|
3. This is an experimental build of ZoneMinder supporting nginx, rather than
|
||||||
and verify it is set to "/cgi-bin-zm/nph-zms". Failure to do so will result
|
apache web server.
|
||||||
in a broken system. You have been warned.
|
|
||||||
|
|
||||||
3. Due to the active state of the ZoneMinder project, we now recommend granting
|
4. If you have installed ZoneMinder from the FedBerry repositories, this build
|
||||||
ALL permission to the ZoneMinder mysql account. This change must be done
|
of ZoneMinder has support for Raspberry Pi hardware acceleration when using
|
||||||
manually before ZoneMinder will run. See the installation steps below.
|
ffmpeg. Unforunately, there is a problem with the same hardware acceleration
|
||||||
|
when using libvlc. Consequently, libvlc support in this build of ZoneMinder
|
||||||
4. This package uses the HTTPS protocol by default to access the web portal.
|
has been disabled until the problem is resolved. See the following bug
|
||||||
Requests using HTTP will auto-redirect to HTTPS. See README.https for
|
report for details: https://trac.videolan.org/vlc/ticket/18594
|
||||||
more information.
|
|
||||||
|
|
||||||
5. This package ships with the new ZoneMinder API enabled.
|
|
||||||
|
|
||||||
New installs
|
New installs
|
||||||
============
|
============
|
||||||
|
|
||||||
1. This package supports either community-mysql-server or mariadb-server with
|
1. Unless you are already using MariaDB server, you need to ensure that the
|
||||||
mariadb being the preferred choice. Unless you are already using MariaDB or
|
server is configured to start during boot and properly secured by running:
|
||||||
Mysql server, you need to ensure that the server is configured to start
|
|
||||||
during boot and properly secured by running:
|
|
||||||
|
|
||||||
sudo dnf install mariadb-server
|
sudo dnf install mariadb-server
|
||||||
sudo systemctl enable mariadb
|
sudo systemctl enable mariadb
|
||||||
|
@ -48,13 +43,17 @@ New installs
|
||||||
anything that suits your environment.
|
anything that suits your environment.
|
||||||
|
|
||||||
3. If you have chosen to change the zoneminder database account credentials to
|
3. If you have chosen to change the zoneminder database account credentials to
|
||||||
something other than zmuser/zmpass, you must now edit /etc/zm/zm.conf.
|
something other than zmuser/zmpass, you must now create a config file under
|
||||||
Change ZM_DB_USER and ZM_DB_PASS to the values you created in the previous
|
/etc/zm/conf.d and set your credentials there. For example, create the file
|
||||||
step.
|
/etc/zm/conf.d/zm-db-user.conf and add the following content to it:
|
||||||
|
|
||||||
This version of zoneminder no longer requires you to make a similar change
|
ZM_DB_USER = {username of the sql account you want to use}
|
||||||
to the credentials in /usr/share/zoneminder/www/api/app/Config/database.php
|
ZM_DB_PASS = {password of the sql account you want to use}
|
||||||
This now happens dynamically. Do *not* make any changes to this file.
|
|
||||||
|
Once the file has been saved, set proper file & ownership permissions on it:
|
||||||
|
|
||||||
|
sudo chown root:apache *.conf
|
||||||
|
sudo chmod 640 *.conf
|
||||||
|
|
||||||
4. Edit /etc/php.ini, uncomment the date.timezone line, and add your local
|
4. Edit /etc/php.ini, uncomment the date.timezone line, and add your local
|
||||||
timezone. PHP will complain loudly if this is not set, or if it is set
|
timezone. PHP will complain loudly if this is not set, or if it is set
|
||||||
|
@ -80,13 +79,36 @@ New installs
|
||||||
SELINUX line from "enforcing" to "disabled". This change will take
|
SELINUX line from "enforcing" to "disabled". This change will take
|
||||||
effect after a reboot.
|
effect after a reboot.
|
||||||
|
|
||||||
6. This package comes preconfigured for HTTPS using the default self signed
|
6. Configure the web server
|
||||||
certificate on your system. We recommend you keep this configuration.
|
|
||||||
|
|
||||||
If this does not meet your needs, then read README.https to
|
This package uses the HTTPS protocol by default to access the web portal,
|
||||||
learn about alternatives.
|
using the default self signed certificate on your system. Requests using
|
||||||
|
HTTP will auto-redirect to HTTPS.
|
||||||
|
|
||||||
7. Edit /etc/sysconfig/fcgiwrap and set DAEMON_PROCS to the maximum number of
|
Inspect the web server configuration file and verify it meets your needs:
|
||||||
|
|
||||||
|
/etc/zm/www/zoneminder.conf
|
||||||
|
|
||||||
|
If you are running other web enabled services then you may need to edit
|
||||||
|
this file to suite. See README.https to learn about other alternatives.
|
||||||
|
|
||||||
|
When in doubt, proceed with the default:
|
||||||
|
|
||||||
|
sudo ln -s /etc/zm/www/zoneminder.conf /etc/nginx/default.d/
|
||||||
|
|
||||||
|
7. Fcgiwrap is required when using ZoneMinder with Nginx. At the time of this
|
||||||
|
writing, fcgiwrap is not yet available in the Fedora repos. Until it
|
||||||
|
becomes available, you may install it from my Copr repository:
|
||||||
|
|
||||||
|
https://copr.fedorainfracloud.org/coprs/kni/fcgiwrap/
|
||||||
|
|
||||||
|
Follow the intructions on that site to enable the repo. Once enabled,
|
||||||
|
install fcgiwrap:
|
||||||
|
|
||||||
|
sudo dnf install fcgiwrap
|
||||||
|
|
||||||
|
After fcgiwrap is installed, it must be configured. Edit
|
||||||
|
/etc/sysconfig/fcgiwrap and set DAEMON_PROCS to the maximum number of
|
||||||
simulatneous streams the server should support. Generally, a good minimum
|
simulatneous streams the server should support. Generally, a good minimum
|
||||||
value for this equals the total number of cameras you expect to view at the
|
value for this equals the total number of cameras you expect to view at the
|
||||||
same time.
|
same time.
|
||||||
|
@ -101,33 +123,43 @@ New installs
|
||||||
sudo systemctl enable zoneminder
|
sudo systemctl enable zoneminder
|
||||||
sudo systemctl start zoneminder
|
sudo systemctl start zoneminder
|
||||||
|
|
||||||
10.The Fedora repos have a ZoneMinder package available, but it does not
|
10. Optionally configure the firewall
|
||||||
support ffmpeg or libvlc, which many modern IP cameras require. Most users
|
|
||||||
will want to prevent the ZoneMinder package in the Fedora repos from
|
All Redhat distros ship with the firewall enabled. That means you will not
|
||||||
overwriting the ZoneMinder package in zmrepo, during a future dnf update. To
|
be able to access the ZoneMinder web console from a remote machine until
|
||||||
prevent that from happening you must edit /etc/yum.repos.d/fedora.repo
|
changes are made to the firewall.
|
||||||
and /etc/yum.repos.d/fedora-updates.repo. Add the line "exclude=zoneminder*"
|
|
||||||
without the quotes under the [fedora] and [fedora-updates] blocks,
|
What follows are a set of minimal commands to allow remote access to the
|
||||||
respectively.
|
ZoneMinder web console and also allow ZoneMinder's ONVIF discovery to
|
||||||
|
work. The following commands do not put any restrictions on which remote
|
||||||
|
machine(s) have access to the listed ports or services.
|
||||||
|
|
||||||
|
sudo firewall-cmd --permanent --zone=public --add-service=http
|
||||||
|
sudo firewall-cmd --permanent --zone=public --add-service=https
|
||||||
|
sudo firewall-cmd --permanent --zone=public --add-port=3702/udp
|
||||||
|
sudo firewall-cmd --reload
|
||||||
|
|
||||||
|
Additional changes to the firewall may be required, depending on your
|
||||||
|
security requirements and how you use the system. It is up to you to verify
|
||||||
|
these commands are sufficient.
|
||||||
|
|
||||||
|
11. Access the ZoneMinder web console
|
||||||
|
|
||||||
|
You may now access the ZoneMinder web console from your web browser using
|
||||||
|
an appropriate url. Here are some examples:
|
||||||
|
|
||||||
|
http://localhost/zm (works from the local machine only)
|
||||||
|
http://{machine name}/zm (works only if dns is configured for your network)
|
||||||
|
http://{ip address}/zm
|
||||||
|
|
||||||
Upgrades
|
Upgrades
|
||||||
========
|
========
|
||||||
|
|
||||||
1. Verify /etc/zm/zm.conf.
|
1. Conf.d folder support has been added to ZoneMinder. Any custom
|
||||||
|
changes previously made to zm.conf must now be made in one or more custom
|
||||||
If zm.conf was manually edited before running the upgrade, the installation
|
config files, created under the conf.d folder. Do this now. See
|
||||||
may not overwrite it. In this case, it will create the file
|
/etc/zm/conf.d/README for details. Once you recreate any custom config changes
|
||||||
/etc/zm/zm.conf.rpmnew.
|
under the conf.d folder, they will remain in place indefinitely.
|
||||||
|
|
||||||
For example, this will happen if you are using database account credentials
|
|
||||||
other than zmuser/zmpass.
|
|
||||||
|
|
||||||
Compare /etc/zm/zm.conf to /etc/zm/zm.conf.rpmnew. Verify that zm.conf
|
|
||||||
contains any new config settings that may be in zm.conf.rpmnew.
|
|
||||||
|
|
||||||
This version of zoneminder no longer requires you to make a similar change
|
|
||||||
to the credentials in /usr/share/zoneminder/www/api/app/Config/database.php
|
|
||||||
This now happens dynamically. Do *not* make any changes to this file.
|
|
||||||
|
|
||||||
2. Verify permissions of the zmuser account.
|
2. Verify permissions of the zmuser account.
|
||||||
|
|
||||||
|
@ -139,12 +171,16 @@ Upgrades
|
||||||
|
|
||||||
See step 2 of the Installation section to add missing permissions.
|
See step 2 of the Installation section to add missing permissions.
|
||||||
|
|
||||||
3. Verify the ZoneMinder Apache configuration file in the folder
|
3. Verify the ZoneMinder Nginx configuration file in the folder
|
||||||
/etc/httpd/conf.d. You will have a file called "zoneminder.conf" and there
|
/etc/zm/www. You will have a file called "zoneminder.conf" and there
|
||||||
may also be a file called "zoneminder.conf.rpmnew". If the rpmnew file
|
may also be a file called "zoneminder.conf.rpmnew". If the rpmnew file
|
||||||
exists, inspect it and merge anything new in that file with zoneminder.conf.
|
exists, inspect it and merge anything new in that file with zoneminder.conf.
|
||||||
Verify the SSL REquirements meet your needs. Read README.https if necessary.
|
Verify the SSL REquirements meet your needs. Read README.https if necessary.
|
||||||
|
|
||||||
|
The contents of this file must be merged into your Nginx configuration.
|
||||||
|
See step 6 of the installation section if you have not already done this
|
||||||
|
during a previous upgrade.
|
||||||
|
|
||||||
4. Upgrade the database before starting ZoneMinder.
|
4. Upgrade the database before starting ZoneMinder.
|
||||||
|
|
||||||
Most upgrades can be performed by executing the following command:
|
Most upgrades can be performed by executing the following command:
|
||||||
|
|
|
@ -22,6 +22,10 @@ location /cgi-bin-zm {
|
||||||
fastcgi_pass unix:/run/fcgiwrap.sock;
|
fastcgi_pass unix:/run/fcgiwrap.sock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /zm/cache {
|
||||||
|
alias "@ZM_CACHEDIR@";
|
||||||
|
}
|
||||||
|
|
||||||
location /zm {
|
location /zm {
|
||||||
gzip off;
|
gzip off;
|
||||||
alias "@ZM_WEBDIR@";
|
alias "@ZM_WEBDIR@";
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
# Change the user and group of the default pool to the web server account
|
; Change the user and group of the default pool to the web server account
|
||||||
[www]
|
[www]
|
||||||
|
|
||||||
user = @WEB_USER@
|
user = @WEB_USER@
|
||||||
group = @WEB_GROUP@
|
group = @WEB_GROUP@
|
||||||
|
|
||||||
# Uncomment these on machines with little memory
|
; These parameters are typically a tradoff between performance and memory
|
||||||
#pm = ondemand
|
; consumption. See the contents of www.conf for details.
|
||||||
#pm.max_children = 10
|
|
||||||
#pm.process_idle_timeout = 10s
|
pm = ondemand
|
||||||
|
pm.max_children = 50
|
||||||
|
pm.process_idle_timeout = 10s
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
D @ZM_TMPDIR@ 0755 @WEB_USER@ @WEB_GROUP@
|
D @ZM_TMPDIR@ 0755 @WEB_USER@ @WEB_GROUP@
|
||||||
D @ZM_SOCKDIR@ 0755 @WEB_USER@ @WEB_GROUP@
|
D @ZM_SOCKDIR@ 0755 @WEB_USER@ @WEB_GROUP@
|
||||||
|
D @ZM_CACHEDIR@ 0755 @WEB_USER@ @WEB_GROUP@
|
||||||
|
d @ZM_DIR_EVENTS@ 0755 @WEB_USER@ @WEB_GROUP@
|
||||||
|
D @ZM_DIR_IMAGES@ 0755 @WEB_USER@ @WEB_GROUP@
|
||||||
D /var/lib/php/session 770 root @WEB_GROUP@
|
D /var/lib/php/session 770 root @WEB_GROUP@
|
||||||
D /var/lib/php/wsdlcache 770 root @WEB_GROUP@
|
D /var/lib/php/wsdlcache 770 root @WEB_GROUP@
|
||||||
|
|
||||||
|
|
|
@ -84,7 +84,6 @@ BuildRequires: libmp4v2-devel
|
||||||
BuildRequires: x264-devel
|
BuildRequires: x264-devel
|
||||||
|
|
||||||
%{?with_nginx:Requires: nginx}
|
%{?with_nginx:Requires: nginx}
|
||||||
%{?with_nginx:Requires: fcgiwrap}
|
|
||||||
%{?with_nginx:Requires: php-fpm}
|
%{?with_nginx:Requires: php-fpm}
|
||||||
%{!?with_nginx:Requires: httpd}
|
%{!?with_nginx:Requires: httpd}
|
||||||
%{!?with_nginx:Requires: php}
|
%{!?with_nginx:Requires: php}
|
||||||
|
@ -131,7 +130,7 @@ designed to support as many cameras as you can attach to your computer without
|
||||||
too much degradation of performance.
|
too much degradation of performance.
|
||||||
|
|
||||||
%prep
|
%prep
|
||||||
%autosetup -p 1 -a 1 -n ZoneMinder-%{version}
|
%autosetup -p 1 -a 1
|
||||||
%{__rm} -rf ./web/api/app/Plugin/Crud
|
%{__rm} -rf ./web/api/app/Plugin/Crud
|
||||||
%{__mv} -f crud-%{crud_version} ./web/api/app/Plugin/Crud
|
%{__mv} -f crud-%{crud_version} ./web/api/app/Plugin/Crud
|
||||||
|
|
||||||
|
@ -306,7 +305,7 @@ EOF
|
||||||
|
|
||||||
%{_libexecdir}/zoneminder/
|
%{_libexecdir}/zoneminder/
|
||||||
%{_datadir}/zoneminder/
|
%{_datadir}/zoneminder/
|
||||||
%{_datadir}/applications/*%{name}.desktop
|
%{_datadir}/applications/*zoneminder.desktop
|
||||||
|
|
||||||
%dir %attr(755,%{zmuid_final},%{zmgid_final}) %{_sharedstatedir}/zoneminder
|
%dir %attr(755,%{zmuid_final},%{zmgid_final}) %{_sharedstatedir}/zoneminder
|
||||||
%dir %attr(755,%{zmuid_final},%{zmgid_final}) %{_sharedstatedir}/zoneminder/events
|
%dir %attr(755,%{zmuid_final},%{zmgid_final}) %{_sharedstatedir}/zoneminder/events
|
||||||
|
|
187
docs/api.rst
187
docs/api.rst
|
@ -5,7 +5,6 @@ This document will provide an overview of ZoneMinder's API. This is work in prog
|
||||||
|
|
||||||
Overview
|
Overview
|
||||||
^^^^^^^^
|
^^^^^^^^
|
||||||
|
|
||||||
In an effort to further 'open up' ZoneMinder, an API was needed. This will
|
In an effort to further 'open up' ZoneMinder, an API was needed. This will
|
||||||
allow quick integration with and development of ZoneMinder.
|
allow quick integration with and development of ZoneMinder.
|
||||||
|
|
||||||
|
@ -13,6 +12,78 @@ The API is built in CakePHP and lives under the ``/api`` directory. It
|
||||||
provides a RESTful service and supports CRUD (create, retrieve, update, delete)
|
provides a RESTful service and supports CRUD (create, retrieve, update, delete)
|
||||||
functions for Monitors, Events, Frames, Zones and Config.
|
functions for Monitors, Events, Frames, Zones and Config.
|
||||||
|
|
||||||
|
Streaming Interface
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
Developers working on their application often ask if there is an "API" to receive live streams, or recorded event streams.
|
||||||
|
It is possible to stream both live and recorded streams. This isn't strictly an "API" per-se (that is, it is not integrated
|
||||||
|
into the Cake PHP based API layer discussed here) and also why we've used the term "Interface" instead of an "API".
|
||||||
|
|
||||||
|
Live Streams
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
What you need to know is that if you want to display "live streams", ZoneMinder sends you streaming JPEG images (MJPEG)
|
||||||
|
which can easily be rendered in a browser using an ``img src`` tag.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
<img src="https://yourserver/zm/cgi-bin/nph-zms?scale=50&width=640p&height=480px&mode=jpeg&maxfps=5&buffer=1000&&monitor=1&auth=b54a589e09f330498f4ae2203&connkey=36139" />
|
||||||
|
|
||||||
|
will display a live feed from monitor id 1, scaled down by 50% in quality and resized to 640x480px.
|
||||||
|
|
||||||
|
* This assumes ``/zm/cgi-bin`` is your CGI_BIN path. Change it to what is correct in your system
|
||||||
|
* The "auth" token you see above is required if you use ZoneMinder authentication. To understand how to get the auth token, please read the "Login, Logout & API security" section below.
|
||||||
|
* The "connkey" parameter is essentially a random number which uniquely identifies a stream. If you don't specify a connkey, ZM will generate its own. It is recommended to generate a connkey because you can then use it to "control" the stream (pause/resume etc.)
|
||||||
|
* Instead of dealing with the "auth" token, you can also use ``&user=username&pass=password`` where "username" and "password" are your ZoneMinder username and password respectively. Note that this is not recommended because you are transmitting them in a URL and even if you use HTTPS, they may show up in web server logs.
|
||||||
|
|
||||||
|
|
||||||
|
PTZ on live streams
|
||||||
|
-------------------
|
||||||
|
PTZ commands are pretty cryptic in ZoneMinder. This is not meant to be an exhaustive guide, but just something to whet your appetite:
|
||||||
|
|
||||||
|
|
||||||
|
Lets assume you have a monitor, with ID=6. Let's further assume you want to pan it left.
|
||||||
|
|
||||||
|
You'd need to send a:
|
||||||
|
``POST`` command to ``https://yourserver/zm/index.php`` with the following data payload in the command (NOT in the URL)
|
||||||
|
|
||||||
|
``view=request&request=control&id=6&control=moveConLeft&xge=30&yge=30``
|
||||||
|
|
||||||
|
Obviously, if you are using authentication, you need to be logged in for this to work.
|
||||||
|
|
||||||
|
Like I said, at this stage, this is only meant to get you started. Explore the ZoneMinder code and use "Inspect source" as you use PTZ commands in the ZoneMinder source code.
|
||||||
|
`control_functions.php <https://github.com/ZoneMinder/zoneminder/blob/10531df54312f52f0f32adec3d4720c063897b62/web/skins/classic/includes/control_functions.php>`__ is a great place to start.
|
||||||
|
|
||||||
|
|
||||||
|
Pre-recorded (past event) streams
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Similar to live playback, if you have chosen to store events in JPEG mode, you can play it back using:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
<img src="https://yourserver/zm/cgi-bin/nph-zms?mode=jpeg&frame=1&replay=none&source=event&event=293820&connkey=77493&auth=b54a58f5f4ae2203" />
|
||||||
|
|
||||||
|
|
||||||
|
* This assumes ``/zm/cgi-bin`` is your CGI_BIN path. Change it to what is correct in your system
|
||||||
|
* This will playback event 293820, starting from frame 1 as an MJPEG stream
|
||||||
|
* Like before, you can add more parameters like ``scale`` etc.
|
||||||
|
* auth and connkey have the same meaning as before, and yes, you can replace auth by ``&user=usename&pass=password`` as before and the same security concerns cited above apply.
|
||||||
|
|
||||||
|
If instead, you have chosen to use the MP4 (Video) storage mode for events, you can directly play back the saved video file:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
<video src="https://yourserver/zm/index.php?view=view_video&eid=294690&auth=33f3d558af84cf08" type="video/mp4"></video>
|
||||||
|
|
||||||
|
* This will play back the video recording for event 294690
|
||||||
|
|
||||||
|
What other parameters are supported?
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
The best way to answer this question is to play with ZoneMinder console. Open a browser, play back live or recorded feed, and do an "Inspect Source" to see what parameters
|
||||||
|
are generated. Change and observe.
|
||||||
|
|
||||||
|
|
||||||
Enabling API
|
Enabling API
|
||||||
^^^^^^^^^^^^
|
^^^^^^^^^^^^
|
||||||
A default ZoneMinder installs with APIs enabled. You can explictly enable/disable the APIs
|
A default ZoneMinder installs with APIs enabled. You can explictly enable/disable the APIs
|
||||||
|
@ -132,6 +203,22 @@ Return a list of all monitors
|
||||||
|
|
||||||
curl http://server/zm/api/monitors.json
|
curl http://server/zm/api/monitors.json
|
||||||
|
|
||||||
|
It is worthwhile to note that starting ZM 1.32.3 and beyond, this API also returns a ``Monitor_Status`` object per monitor. It looks like this:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
"Monitor_Status": {
|
||||||
|
"MonitorId": "2",
|
||||||
|
"Status": "Connected",
|
||||||
|
"CaptureFPS": "1.67",
|
||||||
|
"AnalysisFPS": "1.67",
|
||||||
|
"CaptureBandwidth": "52095"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
If you don't see this in your API, you are running an older version of ZM. This gives you a very convenient way to check monitor status without calling the ``daemonCheck`` API described later.
|
||||||
|
|
||||||
|
|
||||||
Retrieve monitor 1
|
Retrieve monitor 1
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
@ -391,11 +478,13 @@ Create a Zone
|
||||||
&Zone[MaxBlobs]=\
|
&Zone[MaxBlobs]=\
|
||||||
&Zone[OverloadFrames]=0"
|
&Zone[OverloadFrames]=0"
|
||||||
|
|
||||||
PTZ Control APIs
|
PTZ Control Meta-Data APIs
|
||||||
^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
PTZ controls associated with a monitor are stored in the Controls table and not the Monitors table inside ZM. What that means is when you get the details of a Monitor, you will only know if it is controllable (isControllable:true) and the control ID.
|
PTZ controls associated with a monitor are stored in the Controls table and not the Monitors table inside ZM. What that means is when you get the details of a Monitor, you will only know if it is controllable (isControllable:true) and the control ID.
|
||||||
To be able to retrieve PTZ information related to that Control ID, you need to use the controls API
|
To be able to retrieve PTZ information related to that Control ID, you need to use the controls API
|
||||||
|
|
||||||
|
Note that these APIs only retrieve control data related to PTZ. They don't actually move the camera. See the "PTZ on live streams" section to move the camera.
|
||||||
|
|
||||||
This returns all the control definitions:
|
This returns all the control definitions:
|
||||||
::
|
::
|
||||||
|
|
||||||
|
@ -413,7 +502,97 @@ ZM APIs have various APIs that help you in determining host (aka ZM) daemon stat
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
curl -XGET http://server/zm/api/host/daemonCheck.json # 1 = ZM running 0=not running
|
|
||||||
curl -XGET http://server/zm/api/host/getLoad.json # returns current load of ZM
|
curl -XGET http://server/zm/api/host/getLoad.json # returns current load of ZM
|
||||||
|
|
||||||
|
# Note that ZM 1.32.3 onwards has the same information in Monitors.json which is more reliable and works for multi-server too.
|
||||||
|
curl -XGET http://server/zm/api/host/daemonCheck.json # 1 = ZM running 0=not running
|
||||||
|
|
||||||
|
# The API below uses "du" to calculate disk space. We no longer recommend you use it if you have many events. Use the Storage APIs instead, described later
|
||||||
curl -XGET http://server/zm/api/host/getDiskPercent.json # returns in GB (not percentage), disk usage per monitor (that is,space taken to store various event related information,images etc. per monitor)
|
curl -XGET http://server/zm/api/host/getDiskPercent.json # returns in GB (not percentage), disk usage per monitor (that is,space taken to store various event related information,images etc. per monitor)
|
||||||
|
|
||||||
|
|
||||||
|
Storage and Server APIs
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
ZoneMinder introduced many new options that allowed you to configure multiserver/multistorage configurations. While a part of this was available in previous versions, a lot of rework was done as part of ZM 1.31 and 1.32. As part of that work, a lot of new and useful APIs were added. Some of these are part of ZM 1.32 and others will be part of ZM 1.32.3 (of course, if you build from master, you can access them right away, or wait till a stable release is out.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
This returns storage data for my single server install. If you are using multi-storage, you'll see many such "Storage" entries, one for each storage defined:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
curl http://server/zm/api/storage.json
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"storage": [
|
||||||
|
{
|
||||||
|
"Storage": {
|
||||||
|
"Id": "0",
|
||||||
|
"Path": "\/var\/cache\/zoneminder\/events",
|
||||||
|
"Name": "Default",
|
||||||
|
"Type": "local",
|
||||||
|
"Url": null,
|
||||||
|
"DiskSpace": "364705447651",
|
||||||
|
"Scheme": "Medium",
|
||||||
|
"ServerId": null,
|
||||||
|
"DoDelete": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"DiskSpace" is the disk used in bytes. While this doesn't return disk space data as rich as ``/host/getDiskPercent``, it is much more efficient.
|
||||||
|
|
||||||
|
Similarly,
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
curl http://server/zm/api/servers.json
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"Server": {
|
||||||
|
"Id": "1",
|
||||||
|
"Name": "server1",
|
||||||
|
"Hostname": "server1.mydomain.com",
|
||||||
|
"State_Id": null,
|
||||||
|
"Status": "Running",
|
||||||
|
"CpuLoad": "0.9",
|
||||||
|
"TotalMem": "6186237952",
|
||||||
|
"FreeMem": "156102656",
|
||||||
|
"TotalSwap": "536866816",
|
||||||
|
"FreeSwap": "525697024",
|
||||||
|
"zmstats": false,
|
||||||
|
"zmaudit": false,
|
||||||
|
"zmtrigger": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
This only works if you have a multiserver setup in place. If you don't it will return an empty array.
|
||||||
|
|
||||||
|
|
||||||
|
Further Reading
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
As described earlier, treat this document as an "introduction" to the important parts of the API and streaming interfaces.
|
||||||
|
There are several details that haven't yet been documented. Till they are, here are some resources:
|
||||||
|
|
||||||
|
* zmNinja, the open source mobile app for ZoneMinder is 100% based on ZM APIs. Explore its `source code <https://github.com/pliablepixels/zmNinja>`__ to see how things work.
|
||||||
|
* Launch up ZM console in a browser, and do an "Inspect source". See how images are being rendered. Go to the networks tab of the inspect source console and look at network requests that are made when you pause/play/forward streams.
|
||||||
|
* If you still can't find an answer, post your question in the `forums <https://forums.zoneminder.com/index.php>`__ (not the github repo).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -164,6 +164,29 @@ Height (pixels)
|
||||||
Web Site Refresh
|
Web Site Refresh
|
||||||
If the website in question has static content, optionally enter a time period in seconds for ZoneMinder to refresh the content.
|
If the website in question has static content, optionally enter a time period in seconds for ZoneMinder to refresh the content.
|
||||||
|
|
||||||
|
Storage Tab
|
||||||
|
-----------
|
||||||
|
|
||||||
|
The storage section allows for each monitor to configure if and how video and audio are recorded.
|
||||||
|
|
||||||
|
Save JPEGs
|
||||||
|
Records video in individual JPEG frames. Storing JPEG frames requires more storage space than h264 but it allows to view an event anytime while it is being recorded.
|
||||||
|
|
||||||
|
* Disabled – video is not recorded as JPEG frames. If this setting is selected, then "Video Writer" should be enabled otherwise there is no video recording at all.
|
||||||
|
* Frames only – video is recorded in individual JPEG frames.
|
||||||
|
* Analysis images only (if available) – video is recorded in invidual JPEG frames with an overlay of the motion detection analysis information. Note that this overlay remains permanently visible in the frames.
|
||||||
|
* Frames + Analysis images (if available) – video is recorded twice, once as normal individual JPEG frames and once in invidual JPEG frames with analysis information overlaid.
|
||||||
|
|
||||||
|
Video Writer
|
||||||
|
Records video in real video format. It provides much better compression results than saving JPEGs, thus longer video history can be stored.
|
||||||
|
|
||||||
|
* Disabled – video is not recorded in video format. If this setting is selected, then "Save JPEGs" should be enabled otherwise there is no video recording at all.
|
||||||
|
* X264 Encode – the video or picture frames received from the camera are transcoded into h264 and stored as a video. This option is useful if the camera cannot natively stream h264.
|
||||||
|
* H264 Camera Passthrough – this option assumes that the camera is already sending an h264 stream. Video will be recorded as is, without any post-processing in zoneminder. Video characteristics such as bitrate, encoding mode, etc. should be set directly in the camera.
|
||||||
|
|
||||||
|
Recording Audio
|
||||||
|
Check the box labeled "Whether to store the audio stream when saving an event." in order to save audio (if available) when events are recorded.
|
||||||
|
|
||||||
Timestamp Tab
|
Timestamp Tab
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
Introduction
|
Introduction
|
||||||
============
|
============
|
||||||
|
|
||||||
Welcome to ZoneMinder, the all-in-one Linux GPL'd security camera solution.
|
Welcome to ZoneMinder, the all-in-one security camera solution for Linux with GPL License.
|
||||||
|
|
||||||
Most commercial "security systems" are designed as a monitoring system that also records. Recording quality can vary from bad to unusable, locating the relevant video can range from challenging to impractical, and exporting can often only be done with the manual present. ZoneMinder was designed primarily to record, and allow easy searches and exporting. Recordings are of the best possible quality, easy to filter and find, and simple to export using any system with a web browser. It also monitors.
|
Commercial "security systems" are often designed as a monitoring system with little attention to recording quality. In such a system, locating and exporting relevant video can be challenging and often requires extensive human intervention. ZoneMinder was designed to provide the best possible record quality while allowing easy searching, filtering and exporting of security footage.
|
||||||
|
|
||||||
ZoneMinder is designed around a series of independent components that only function when necessary limiting any wasted resource and maximising the efficiency of your machine. A fairly ancient Pentium II PC should be able to track one camera per device at up to 25 frames per second with this dropping by half approximately for each additional camera on the same device. Additional cameras on other devices do not interact so can maintain this frame rate. Even monitoring several cameras still will not overload the CPU as frame processing is designed to synchronise with capture and not stall it.
|
ZoneMinder is designed around a series of independent components that only function when necessary, limiting any wasted resource and maximising the efficiency of your machine. An outdated Pentium II PC can have multiple recording devices connected to it, and it is able to track one camera per device at up to 25 frames per second, which drops by approximately half for each additional camera on the same device. Additional cameras on devices that do not interact with other devices can maintain the 25 frame rate per second. Monitoring several cameras will not overload the CPU as frame processing is designed to synchronise with capture.
|
||||||
|
|
||||||
As well as being fast ZoneMinder is designed to be friendly and even more than that, actually useful. As well as the fast video interface core it also comes with a user friendly and comprehensive PHP based web interface allowing you to control and monitor your cameras from home, at work, on the road, or even a web enabled cell phone. It supports variable web capabilities based on available bandwidth. The web interface also allows you to view events that your cameras have captured and archive them or review them time and again, or delete the ones you no longer wish to keep. The web pages directly interact with the core daemons ensuring full co-operation at all times. ZoneMinder can even be installed as a system service ensuring it is right there if your computer has to reboot for any reason.
|
A fast video interface core, a user-friendly and comprehensive PHP based web interface allows ZoneMinder to be efficient, friendly and most importantly useful. You can control and monitor your cameras from home, at work, on the road, or a web-enabled cell phone. It supports variable web capabilities based on available bandwidth. The web interface also allows you to view events that your cameras have captured, which can be archived, reviewed or deleted. The web application directly interacts with the core daemons ensuring full co-operation at all times. ZoneMinder can also be installed as a system service to reboot a system remotely.
|
||||||
|
|
||||||
The core of ZoneMinder is the capture and analysis of images and there is a highly configurable set of parameters that allow you to ensure that you can eliminate false positives whilst ensuring that anything you don't want to miss will be captured and saved. ZoneMinder allows you to define a set of 'zones' for each camera of varying sensitivity and functionality. This allows you to eliminate regions that you don't wish to track or define areas that will alarm if various thresholds are exceeded in conjunction with other zones.
|
The core of ZoneMinder is the capture and analysis of images and a highly configurable set of parameters that eliminate false positives whilst ensuring minimum loss of footage. For example, you can define a set of 'zones' for each camera of varying sensitivity and functionality. This eliminates zones that you don't wish to track or define areas that will alarm if various thresholds are exceeded in conjunction with other zones.
|
||||||
|
|
||||||
ZoneMinder is free, but if you do find it useful then please feel free to visit http://www.zoneminder.com/donate.html and help to fund future improvements to ZoneMinder.
|
|
||||||
|
|
||||||
|
ZoneMinder is free under GPL License, but if you do find it useful, then please feel free to visit http://www.zoneminder.com/donate.html and help us fund our future improvements.
|
||||||
|
|
|
@ -309,6 +309,8 @@ saving configuration is a convenient way to ensure that the configuration
|
||||||
held in the database corresponds with the most recent definitions and that
|
held in the database corresponds with the most recent definitions and that
|
||||||
all components are using the same set of configuration.
|
all components are using the same set of configuration.
|
||||||
|
|
||||||
|
=back
|
||||||
|
|
||||||
=head2 EXPORT
|
=head2 EXPORT
|
||||||
|
|
||||||
None by default.
|
None by default.
|
||||||
|
|
|
@ -41,17 +41,18 @@ our @ISA = qw(Exporter ZoneMinder::Base);
|
||||||
# If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
|
# If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
|
||||||
# will save memory.
|
# will save memory.
|
||||||
our %EXPORT_TAGS = (
|
our %EXPORT_TAGS = (
|
||||||
'functions' => [ qw(
|
functions => [ qw(
|
||||||
zmDbConnect
|
zmDbConnect
|
||||||
zmDbDisconnect
|
zmDbDisconnect
|
||||||
zmDbGetMonitors
|
zmDbGetMonitors
|
||||||
zmDbGetMonitor
|
zmDbGetMonitor
|
||||||
zmDbGetMonitorAndControl
|
zmDbGetMonitorAndControl
|
||||||
|
zmDbDo
|
||||||
) ]
|
) ]
|
||||||
);
|
);
|
||||||
push( @{$EXPORT_TAGS{all}}, @{$EXPORT_TAGS{$_}} ) foreach keys %EXPORT_TAGS;
|
push( @{$EXPORT_TAGS{all}}, @{$EXPORT_TAGS{$_}} ) foreach keys %EXPORT_TAGS;
|
||||||
|
|
||||||
our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
|
our @EXPORT_OK = ( @{ $EXPORT_TAGS{all} } );
|
||||||
|
|
||||||
our @EXPORT = qw();
|
our @EXPORT = qw();
|
||||||
|
|
||||||
|
@ -66,8 +67,6 @@ our $VERSION = $ZoneMinder::Base::VERSION;
|
||||||
use ZoneMinder::Logger qw(:all);
|
use ZoneMinder::Logger qw(:all);
|
||||||
use ZoneMinder::Config qw(:all);
|
use ZoneMinder::Config qw(:all);
|
||||||
|
|
||||||
use Carp;
|
|
||||||
|
|
||||||
our $dbh = undef;
|
our $dbh = undef;
|
||||||
|
|
||||||
sub zmDbConnect {
|
sub zmDbConnect {
|
||||||
|
@ -93,7 +92,7 @@ sub zmDbConnect {
|
||||||
|
|
||||||
my $sslOptions = '';
|
my $sslOptions = '';
|
||||||
if ( $Config{ZM_DB_SSL_CA_CERT} ) {
|
if ( $Config{ZM_DB_SSL_CA_CERT} ) {
|
||||||
$sslOptions = ';'.join(';',
|
$sslOptions = join(';','',
|
||||||
'mysql_ssl=1',
|
'mysql_ssl=1',
|
||||||
'mysql_ssl_ca_file='.$Config{ZM_DB_SSL_CA_CERT},
|
'mysql_ssl_ca_file='.$Config{ZM_DB_SSL_CA_CERT},
|
||||||
'mysql_ssl_client_key='.$Config{ZM_DB_SSL_CLIENT_KEY},
|
'mysql_ssl_client_key='.$Config{ZM_DB_SSL_CLIENT_KEY},
|
||||||
|
@ -102,8 +101,9 @@ sub zmDbConnect {
|
||||||
}
|
}
|
||||||
|
|
||||||
eval {
|
eval {
|
||||||
$dbh = DBI->connect( 'DBI:mysql:database='.$Config{ZM_DB_NAME}
|
$dbh = DBI->connect(
|
||||||
.$socket . $sslOptions . ($options?';'.join(';', map { $_.'='.$$options{$_} } keys %{$options} ) : '')
|
'DBI:mysql:database='.$Config{ZM_DB_NAME}
|
||||||
|
.$socket . $sslOptions . ($options?join(';', '', map { $_.'='.$$options{$_} } keys %{$options} ) : '')
|
||||||
, $Config{ZM_DB_USER}
|
, $Config{ZM_DB_USER}
|
||||||
, $Config{ZM_DB_PASS}
|
, $Config{ZM_DB_PASS}
|
||||||
);
|
);
|
||||||
|
@ -125,7 +125,7 @@ sub zmDbConnect {
|
||||||
|
|
||||||
sub zmDbDisconnect {
|
sub zmDbDisconnect {
|
||||||
if ( defined( $dbh ) ) {
|
if ( defined( $dbh ) ) {
|
||||||
$dbh->disconnect();
|
$dbh->disconnect() or Error('Error disconnecting db? ' . $dbh->errstr());
|
||||||
$dbh = undef;
|
$dbh = undef;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,7 +141,7 @@ sub zmDbGetMonitors {
|
||||||
zmDbConnect();
|
zmDbConnect();
|
||||||
|
|
||||||
my $function = shift || DB_MON_ALL;
|
my $function = shift || DB_MON_ALL;
|
||||||
my $sql = "select * from Monitors";
|
my $sql = 'SELECT * FROM Monitors';
|
||||||
|
|
||||||
if ( $function ) {
|
if ( $function ) {
|
||||||
if ( $function == DB_MON_CAPT ) {
|
if ( $function == DB_MON_CAPT ) {
|
||||||
|
@ -156,26 +156,38 @@ sub zmDbGetMonitors {
|
||||||
$sql .= " where Function = 'Nodect'";
|
$sql .= " where Function = 'Nodect'";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
my $sth = $dbh->prepare_cached( $sql )
|
my $sth = $dbh->prepare_cached( $sql );
|
||||||
or croak( "Can't prepare '$sql': ".$dbh->errstr() );
|
if ( ! $sth ) {
|
||||||
my $res = $sth->execute()
|
Error("Can't prepare '$sql': ".$dbh->errstr());
|
||||||
or croak( "Can't execute '$sql': ".$sth->errstr() );
|
return undef;
|
||||||
|
}
|
||||||
|
my $res = $sth->execute();
|
||||||
|
if ( ! $res ) {
|
||||||
|
Error("Can't execute '$sql': ".$sth->errstr());
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
|
|
||||||
my @monitors;
|
my @monitors;
|
||||||
while( my $monitor = $sth->fetchrow_hashref() ) {
|
while( my $monitor = $sth->fetchrow_hashref() ) {
|
||||||
push( @monitors, $monitor );
|
push( @monitors, $monitor );
|
||||||
}
|
}
|
||||||
$sth->finish();
|
$sth->finish();
|
||||||
return( \@monitors );
|
return \@monitors;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub zmSQLExecute {
|
sub zmSQLExecute {
|
||||||
my $sql = shift;
|
my $sql = shift;
|
||||||
|
|
||||||
my $sth = $dbh->prepare_cached( $sql )
|
my $sth = $dbh->prepare_cached( $sql );
|
||||||
or croak( "Can't prepare '$sql': ".$dbh->errstr() );
|
if ( ! $sth ) {
|
||||||
my $res = $sth->execute( @_ )
|
Error("Can't prepare '$sql': ".$dbh->errstr());
|
||||||
or croak( "Can't execute '$sql': ".$sth->errstr() );
|
return undef;
|
||||||
|
}
|
||||||
|
my $res = $sth->execute( @_ );
|
||||||
|
if ( ! $res ) {
|
||||||
|
Error("Can't execute '$sql': ".$sth->errstr());
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,17 +197,22 @@ sub zmDbGetMonitor {
|
||||||
my $id = shift;
|
my $id = shift;
|
||||||
|
|
||||||
if ( !defined($id) ) {
|
if ( !defined($id) ) {
|
||||||
croak("Undefined id in zmDbgetMonitor");
|
Error('Undefined id in zmDbgetMonitor');
|
||||||
return undef ;
|
return undef ;
|
||||||
}
|
}
|
||||||
|
|
||||||
my $sql = 'SELECT * FROM Monitors WHERE Id = ?';
|
my $sql = 'SELECT * FROM Monitors WHERE Id = ?';
|
||||||
my $sth = $dbh->prepare_cached($sql)
|
my $sth = $dbh->prepare_cached($sql);
|
||||||
or croak("Can't prepare '$sql': ".$dbh->errstr());
|
if ( !$sth ) {
|
||||||
my $res = $sth->execute($id)
|
Error("Can't prepare '$sql': ".$dbh->errstr());
|
||||||
or croak("Can't execute '$sql': ".$sth->errstr());
|
return undef;
|
||||||
|
}
|
||||||
|
my $res = $sth->execute($id);
|
||||||
|
if ( $res ) {
|
||||||
|
Error("Can't execute '$sql': ".$sth->errstr());
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
my $monitor = $sth->fetchrow_hashref();
|
my $monitor = $sth->fetchrow_hashref();
|
||||||
|
|
||||||
return $monitor;
|
return $monitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,25 +221,28 @@ sub zmDbGetMonitorAndControl {
|
||||||
|
|
||||||
my $id = shift;
|
my $id = shift;
|
||||||
|
|
||||||
return( undef ) if ( !defined($id) );
|
return undef if !defined($id);
|
||||||
|
|
||||||
my $sql = "SELECT C.*,M.*,C.Protocol
|
my $sql = 'SELECT C.*,M.*,C.Protocol
|
||||||
FROM Monitors as M
|
FROM Monitors as M
|
||||||
INNER JOIN Controls as C on (M.ControlId = C.Id)
|
INNER JOIN Controls as C on (M.ControlId = C.Id)
|
||||||
WHERE M.Id = ?"
|
WHERE M.Id = ?'
|
||||||
;
|
;
|
||||||
my $sth = $dbh->prepare_cached( $sql )
|
my $sth = $dbh->prepare_cached($sql);
|
||||||
or Fatal( "Can't prepare '$sql': ".$dbh->errstr() );
|
if ( !$sth ) {
|
||||||
my $res = $sth->execute( $id )
|
Error("Can't prepare '$sql': ".$dbh->errstr());
|
||||||
or Fatal( "Can't execute '$sql': ".$sth->errstr() );
|
return undef;
|
||||||
|
}
|
||||||
|
my $res = $sth->execute( $id );
|
||||||
|
if ( !$res ) {
|
||||||
|
Error("Can't execute '$sql': ".$sth->errstr());
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
my $monitor = $sth->fetchrow_hashref();
|
my $monitor = $sth->fetchrow_hashref();
|
||||||
|
return $monitor;
|
||||||
return( $monitor );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub start_transaction {
|
sub start_transaction {
|
||||||
#my ( $caller, undef, $line ) = caller;
|
|
||||||
#$openprint::log->debug("Called start_transaction from $caller : $line");
|
|
||||||
my $d = shift;
|
my $d = shift;
|
||||||
$d = $dbh if ! $d;
|
$d = $dbh if ! $d;
|
||||||
my $ac = $d->{AutoCommit};
|
my $ac = $d->{AutoCommit};
|
||||||
|
@ -231,20 +251,29 @@ sub start_transaction {
|
||||||
} # end sub start_transaction
|
} # end sub start_transaction
|
||||||
|
|
||||||
sub end_transaction {
|
sub end_transaction {
|
||||||
#my ( $caller, undef, $line ) = caller;
|
|
||||||
#$openprint::log->debug("Called end_transaction from $caller : $line");
|
|
||||||
my ( $d, $ac ) = @_;
|
my ( $d, $ac ) = @_;
|
||||||
if ( ! defined $ac ) {
|
if ( ! defined $ac ) {
|
||||||
Error("Undefined ac");
|
Error("Undefined ac");
|
||||||
}
|
}
|
||||||
$d = $dbh if ! $d;
|
$d = $dbh if ! $d;
|
||||||
if ( $ac ) {
|
if ( $ac ) {
|
||||||
#$log->debug("Committing");
|
|
||||||
$d->commit();
|
$d->commit();
|
||||||
} # end if
|
} # end if
|
||||||
$d->{AutoCommit} = $ac;
|
$d->{AutoCommit} = $ac;
|
||||||
} # end sub end_transaction
|
} # end sub end_transaction
|
||||||
|
|
||||||
|
# Basic execution of $dbh->do but with some pretty logging of the sql on error.
|
||||||
|
# Returns 1 on success, 0 on error
|
||||||
|
sub zmDbDo {
|
||||||
|
my $sql = shift;
|
||||||
|
if ( ! $dbh->do($sql, undef, @_) ) {
|
||||||
|
$sql =~ s/\?/'%s'/;
|
||||||
|
Error(sprintf("Failed $sql :", @_).$dbh->errstr());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
__END__
|
__END__
|
||||||
|
|
||||||
|
@ -266,6 +295,7 @@ zmDbDisconnect
|
||||||
zmDbGetMonitors
|
zmDbGetMonitors
|
||||||
zmDbGetMonitor
|
zmDbGetMonitor
|
||||||
zmDbGetMonitorAndControl
|
zmDbGetMonitorAndControl
|
||||||
|
zmDbDo
|
||||||
|
|
||||||
=head1 AUTHOR
|
=head1 AUTHOR
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,7 @@ $serial = $primary_key = 'Id';
|
||||||
Frames
|
Frames
|
||||||
AlarmFrames
|
AlarmFrames
|
||||||
DefaultVideo
|
DefaultVideo
|
||||||
|
SaveJPEGs
|
||||||
TotScore
|
TotScore
|
||||||
AvgScore
|
AvgScore
|
||||||
MaxScore
|
MaxScore
|
||||||
|
@ -87,6 +88,7 @@ $serial = $primary_key = 'Id';
|
||||||
Orientation
|
Orientation
|
||||||
DiskSpace
|
DiskSpace
|
||||||
SaveJPEGs
|
SaveJPEGs
|
||||||
|
Scheme
|
||||||
);
|
);
|
||||||
%defaults = (
|
%defaults = (
|
||||||
Cause => q`'Unknown'`,
|
Cause => q`'Unknown'`,
|
||||||
|
@ -285,11 +287,11 @@ sub GenerateVideo {
|
||||||
my $file_size = 'S'.$size;
|
my $file_size = 'S'.$size;
|
||||||
push( @file_parts, $file_size );
|
push( @file_parts, $file_size );
|
||||||
}
|
}
|
||||||
my $video_file = "$video_name-".$file_parts[0]."-".$file_parts[1].".$format";
|
my $video_file = join('-', $video_name, $file_parts[0], $file_parts[1] ).'.'.$format;
|
||||||
if ( $overwrite || !-s $video_file ) {
|
if ( $overwrite || !-s $video_file ) {
|
||||||
Info( "Creating video file $video_file for event $self->{Id}\n" );
|
Info("Creating video file $video_file for event $self->{Id}");
|
||||||
|
|
||||||
my $frame_rate = sprintf( "%.2f", $self->{Frames}/$self->{FullLength} );
|
my $frame_rate = sprintf('%.2f', $self->{Frames}/$self->{FullLength});
|
||||||
if ( $rate ) {
|
if ( $rate ) {
|
||||||
if ( $rate != 1.0 ) {
|
if ( $rate != 1.0 ) {
|
||||||
$frame_rate *= $rate;
|
$frame_rate *= $rate;
|
||||||
|
@ -340,6 +342,7 @@ sub GenerateVideo {
|
||||||
|
|
||||||
sub delete {
|
sub delete {
|
||||||
my $event = $_[0];
|
my $event = $_[0];
|
||||||
|
<<<<<<< HEAD
|
||||||
if ( ! ( $event->{Id} and $event->{MonitorId} and $event->{StartTime} ) ) {
|
if ( ! ( $event->{Id} and $event->{MonitorId} and $event->{StartTime} ) ) {
|
||||||
my ( $caller, undef, $line ) = caller;
|
my ( $caller, undef, $line ) = caller;
|
||||||
Warning("Can't delete event $event->{Id} from Monitor $event->{MonitorId} StartTime:$event->{StartTime} from $caller:$line");
|
Warning("Can't delete event $event->{Id} from Monitor $event->{MonitorId} StartTime:$event->{StartTime} from $caller:$line");
|
||||||
|
@ -351,45 +354,50 @@ sub delete {
|
||||||
}
|
}
|
||||||
Info("Deleting event $event->{Id} from Monitor $event->{MonitorId} StartTime:$event->{StartTime}");
|
Info("Deleting event $event->{Id} from Monitor $event->{MonitorId} StartTime:$event->{StartTime}");
|
||||||
$ZoneMinder::Database::dbh->ping();
|
$ZoneMinder::Database::dbh->ping();
|
||||||
|
=======
|
||||||
|
|
||||||
|
my $in_zmaudit = ( $0 =~ 'zmaudit.pl$');
|
||||||
|
|
||||||
|
if ( ! $in_zmaudit ) {
|
||||||
|
if ( ! ( $event->{Id} and $event->{MonitorId} and $event->{StartTime} ) ) {
|
||||||
|
# zmfilter shouldn't delete anything in an odd situation. zmaudit will though.
|
||||||
|
my ( $caller, undef, $line ) = caller;
|
||||||
|
Warning("$0 Can't Delete event $event->{Id} from Monitor $event->{MonitorId} StartTime:".
|
||||||
|
(defined($event->{StartTime})?$event->{StartTime}:'undef')." from $caller:$line");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ( !($event->Storage()->Path() and -e $event->Storage()->Path()) ) {
|
||||||
|
Warning('Not deleting event because storage path doesn\'t exist');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>>>>>>> master
|
||||||
|
|
||||||
|
if ( $$event{Id} ) {
|
||||||
|
# Need to have an event Id if we are to delete from the db.
|
||||||
|
Info("Deleting event $event->{Id} from Monitor $event->{MonitorId} StartTime:$event->{StartTime}");
|
||||||
|
$ZoneMinder::Database::dbh->ping();
|
||||||
|
|
||||||
$ZoneMinder::Database::dbh->begin_work();
|
$ZoneMinder::Database::dbh->begin_work();
|
||||||
#$event->lock_and_load();
|
#$event->lock_and_load();
|
||||||
|
|
||||||
{
|
ZoneMinder::Database::zmDbDo('DELETE FROM Frames WHERE EventId=?', $$event{Id});
|
||||||
my $sql = 'DELETE FROM Frames WHERE EventId=?';
|
|
||||||
my $sth = $ZoneMinder::Database::dbh->prepare_cached($sql)
|
|
||||||
or Error( "Can't prepare '$sql': ".$ZoneMinder::Database::dbh->errstr() );
|
|
||||||
my $res = $sth->execute($event->{Id})
|
|
||||||
or Error( "Can't execute '$sql': ".$sth->errstr() );
|
|
||||||
$sth->finish();
|
|
||||||
if ( $ZoneMinder::Database::dbh->errstr() ) {
|
if ( $ZoneMinder::Database::dbh->errstr() ) {
|
||||||
$ZoneMinder::Database::dbh->commit();
|
$ZoneMinder::Database::dbh->commit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ZoneMinder::Database::zmDbDo('DELETE FROM Stats WHERE EventId=?', $$event{Id});
|
||||||
$sql = 'DELETE FROM Stats WHERE EventId=?';
|
|
||||||
$sth = $ZoneMinder::Database::dbh->prepare_cached($sql)
|
|
||||||
or Error("Can't prepare '$sql': ".$ZoneMinder::Database::dbh->errstr());
|
|
||||||
$res = $sth->execute($event->{Id})
|
|
||||||
or Error("Can't execute '$sql': ".$sth->errstr());
|
|
||||||
$sth->finish();
|
|
||||||
if ( $ZoneMinder::Database::dbh->errstr() ) {
|
if ( $ZoneMinder::Database::dbh->errstr() ) {
|
||||||
$ZoneMinder::Database::dbh->commit();
|
$ZoneMinder::Database::dbh->commit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
# Do it individually to avoid locking up the table for new events
|
# Do it individually to avoid locking up the table for new events
|
||||||
{
|
ZoneMinder::Database::zmDbDo('DELETE FROM Events WHERE Id=?', $$event{Id});
|
||||||
my $sql = 'DELETE FROM Events WHERE Id=?';
|
|
||||||
my $sth = $ZoneMinder::Database::dbh->prepare_cached($sql)
|
|
||||||
or Error("Can't prepare '$sql': ".$ZoneMinder::Database::dbh->errstr());
|
|
||||||
my $res = $sth->execute($event->{Id})
|
|
||||||
or Error("Can't execute '$sql': ".$sth->errstr());
|
|
||||||
$sth->finish();
|
|
||||||
}
|
|
||||||
$ZoneMinder::Database::dbh->commit();
|
$ZoneMinder::Database::dbh->commit();
|
||||||
if ( (! $Config{ZM_OPT_FAST_DELETE}) and $event->Storage()->DoDelete() ) {
|
}
|
||||||
|
|
||||||
|
if ( ( $in_zmaudit or (!$Config{ZM_OPT_FAST_DELETE})) and $event->Storage()->DoDelete() ) {
|
||||||
$event->delete_files();
|
$event->delete_files();
|
||||||
} else {
|
} else {
|
||||||
Debug('Not deleting event files from '.$event->Path().' for speed.');
|
Debug('Not deleting event files from '.$event->Path().' for speed.');
|
||||||
|
@ -435,7 +443,7 @@ sub delete_files {
|
||||||
if ( $bucket->delete_key($event_path) ) {
|
if ( $bucket->delete_key($event_path) ) {
|
||||||
$deleted = 1;
|
$deleted = 1;
|
||||||
} else {
|
} else {
|
||||||
Error("Failed to delete from S3:".$s3->err . ": " . $s3->errstr);
|
Error('Failed to delete from S3:'.$s3->err . ': ' . $s3->errstr);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Error($@) if $@;
|
Error($@) if $@;
|
||||||
|
@ -471,7 +479,7 @@ sub check_for_in_filesystem {
|
||||||
if ( $path ) {
|
if ( $path ) {
|
||||||
if ( -e $path ) {
|
if ( -e $path ) {
|
||||||
my @files = glob "$path/*";
|
my @files = glob "$path/*";
|
||||||
Debug("Checking for files for event $_[0]{Id} at $path using glob $path/* found " . scalar @files . " files");
|
Debug("Checking for files for event $_[0]{Id} at $path using glob $path/* found " . scalar @files . ' files');
|
||||||
return 1 if @files;
|
return 1 if @files;
|
||||||
} else {
|
} else {
|
||||||
Warning("Path not found for Event $_[0]{Id} at $path");
|
Warning("Path not found for Event $_[0]{Id} at $path");
|
||||||
|
@ -542,7 +550,7 @@ sub MoveTo {
|
||||||
|
|
||||||
if ( $$OldStorage{Id} != $$self{StorageId} ) {
|
if ( $$OldStorage{Id} != $$self{StorageId} ) {
|
||||||
$ZoneMinder::Database::dbh->commit();
|
$ZoneMinder::Database::dbh->commit();
|
||||||
return "Old Storage path changed, Event has moved somewhere else.";
|
return 'Old Storage path changed, Event has moved somewhere else.';
|
||||||
}
|
}
|
||||||
|
|
||||||
$$self{Storage} = $NewStorage;
|
$$self{Storage} = $NewStorage;
|
||||||
|
@ -586,11 +594,11 @@ Debug("Files to move @files");
|
||||||
Debug("Moving file $file to $NewPath");
|
Debug("Moving file $file to $NewPath");
|
||||||
my $size = -s $file;
|
my $size = -s $file;
|
||||||
if ( ! $size ) {
|
if ( ! $size ) {
|
||||||
Info("Not moving file with 0 size");
|
Info('Not moving file with 0 size');
|
||||||
}
|
}
|
||||||
my $file_contents = File::Slurp::read_file($file);
|
my $file_contents = File::Slurp::read_file($file);
|
||||||
if ( ! $file_contents ) {
|
if ( ! $file_contents ) {
|
||||||
die "Loaded empty file, but it had a size. Giving up";
|
die 'Loaded empty file, but it had a size. Giving up';
|
||||||
}
|
}
|
||||||
|
|
||||||
my $filename = $event_path.'/'.File::Basename::basename($file);
|
my $filename = $event_path.'/'.File::Basename::basename($file);
|
||||||
|
@ -598,7 +606,7 @@ Debug("Files to move @files");
|
||||||
die "Unable to add key for $filename";
|
die "Unable to add key for $filename";
|
||||||
}
|
}
|
||||||
my $duration = time - $starttime;
|
my $duration = time - $starttime;
|
||||||
Debug("PUT to S3 " . Number::Bytes::Human::format_bytes($size) . " in $duration seconds = " . Number::Bytes::Human::format_bytes($duration?$size/$duration:$size) . '/sec');
|
Debug('PUT to S3 ' . Number::Bytes::Human::format_bytes($size) . " in $duration seconds = " . Number::Bytes::Human::format_bytes($duration?$size/$duration:$size) . '/sec');
|
||||||
} # end foreach file.
|
} # end foreach file.
|
||||||
|
|
||||||
$moved = 1;
|
$moved = 1;
|
||||||
|
|
|
@ -1,27 +1,3 @@
|
||||||
# ==========================================================================
|
|
||||||
#
|
|
||||||
# ZoneMinder General Utility Module, $Date$, $Revision$
|
|
||||||
# Copyright (C) 2001-2008 Philip Coombes
|
|
||||||
#
|
|
||||||
# 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 common definitions and functions used by the rest
|
|
||||||
# of the ZoneMinder scripts
|
|
||||||
#
|
|
||||||
package ZoneMinder::General;
|
package ZoneMinder::General;
|
||||||
|
|
||||||
use 5.006;
|
use 5.006;
|
||||||
|
@ -42,7 +18,7 @@ our @ISA = qw(Exporter ZoneMinder::Base);
|
||||||
# If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
|
# If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
|
||||||
# will save memory.
|
# will save memory.
|
||||||
our %EXPORT_TAGS = (
|
our %EXPORT_TAGS = (
|
||||||
'functions' => [ qw(
|
functions => [ qw(
|
||||||
executeShellCommand
|
executeShellCommand
|
||||||
getCmdFormat
|
getCmdFormat
|
||||||
runCommand
|
runCommand
|
||||||
|
@ -56,7 +32,7 @@ our %EXPORT_TAGS = (
|
||||||
);
|
);
|
||||||
push( @{$EXPORT_TAGS{all}}, @{$EXPORT_TAGS{$_}} ) foreach keys %EXPORT_TAGS;
|
push( @{$EXPORT_TAGS{all}}, @{$EXPORT_TAGS{$_}} ) foreach keys %EXPORT_TAGS;
|
||||||
|
|
||||||
our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
|
our @EXPORT_OK = ( @{ $EXPORT_TAGS{all} } );
|
||||||
|
|
||||||
our @EXPORT = qw();
|
our @EXPORT = qw();
|
||||||
|
|
||||||
|
@ -80,74 +56,74 @@ sub executeShellCommand {
|
||||||
my $output = qx( $command );
|
my $output = qx( $command );
|
||||||
my $status = $? >> 8;
|
my $status = $? >> 8;
|
||||||
if ( $status || logDebugging() ) {
|
if ( $status || logDebugging() ) {
|
||||||
Debug( "Command: $command\n" );
|
Debug("Command: $command");
|
||||||
chomp( $output );
|
chomp( $output );
|
||||||
Debug( "Output: $output\n" );
|
Debug("Output: $output");
|
||||||
}
|
}
|
||||||
return( $status );
|
return $status;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub getCmdFormat {
|
sub getCmdFormat {
|
||||||
Debug( "Testing valid shell syntax\n" );
|
Debug("Testing valid shell syntax");
|
||||||
|
|
||||||
my ( $name ) = getpwuid( $> );
|
my ( $name ) = getpwuid( $> );
|
||||||
if ( $name eq $Config{ZM_WEB_USER} ) {
|
if ( $name eq $Config{ZM_WEB_USER} ) {
|
||||||
Debug( "Running as '$name', su commands not needed\n" );
|
Debug("Running as '$name', su commands not needed");
|
||||||
return( "" );
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
my $null_command = "true";
|
my $null_command = 'true';
|
||||||
|
|
||||||
my $prefix = "sudo -u ".$Config{ZM_WEB_USER}." ";
|
my $prefix = 'sudo -u '.$Config{ZM_WEB_USER}.' ';
|
||||||
my $suffix = "";
|
my $suffix = '';
|
||||||
my $command = $prefix.$null_command.$suffix;
|
my $command = $prefix.$null_command.$suffix;
|
||||||
Debug( "Testing \"$command\"\n" );
|
Debug("Testing \"$command\"");
|
||||||
my $output = qx($command 2>&1);
|
my $output = qx($command 2>&1);
|
||||||
my $status = $? >> 8;
|
my $status = $? >> 8;
|
||||||
$output //= $!;
|
$output //= $!;
|
||||||
|
|
||||||
if ( !$status ) {
|
if ( !$status ) {
|
||||||
Debug( "Test ok, using format \"$prefix<command>$suffix\"\n" );
|
Debug("Test ok, using format \"$prefix<command>$suffix\"");
|
||||||
return( $prefix, $suffix );
|
return( $prefix, $suffix );
|
||||||
} else {
|
} else {
|
||||||
chomp( $output );
|
chomp( $output );
|
||||||
Debug( "Test failed, '$output'\n" );
|
Debug("Test failed, '$output'");
|
||||||
|
|
||||||
$prefix = "su ".$Config{ZM_WEB_USER}." --shell=/bin/sh --command='";
|
$prefix = 'su '.$Config{ZM_WEB_USER}.q` --shell=/bin/sh --command='`;
|
||||||
$suffix = "'";
|
$suffix = q`'`;
|
||||||
$command = $prefix.$null_command.$suffix;
|
$command = $prefix.$null_command.$suffix;
|
||||||
Debug( "Testing \"$command\"\n" );
|
Debug("Testing \"$command\"");
|
||||||
my $output = qx($command 2>&1);
|
my $output = qx($command 2>&1);
|
||||||
my $status = $? >> 8;
|
my $status = $? >> 8;
|
||||||
$output //= $!;
|
$output //= $!;
|
||||||
|
|
||||||
if ( !$status ) {
|
if ( !$status ) {
|
||||||
Debug( "Test ok, using format \"$prefix<command>$suffix\"\n" );
|
Debug("Test ok, using format \"$prefix<command>$suffix\"");
|
||||||
return( $prefix, $suffix );
|
return( $prefix, $suffix );
|
||||||
} else {
|
} else {
|
||||||
chomp($output);
|
chomp($output);
|
||||||
Debug( "Test failed, '$output'\n" );
|
Debug("Test failed, '$output'");
|
||||||
|
|
||||||
$prefix = "su ".$Config{ZM_WEB_USER}." -c '";
|
$prefix = "su ".$Config{ZM_WEB_USER}." -c '";
|
||||||
$suffix = "'";
|
$suffix = "'";
|
||||||
$command = $prefix.$null_command.$suffix;
|
$command = $prefix.$null_command.$suffix;
|
||||||
Debug( "Testing \"$command\"\n" );
|
Debug("Testing \"$command\"");
|
||||||
$output = qx($command 2>&1);
|
$output = qx($command 2>&1);
|
||||||
$status = $? >> 8;
|
$status = $? >> 8;
|
||||||
$output //= $!;
|
$output //= $!;
|
||||||
|
|
||||||
if ( !$status ) {
|
if ( !$status ) {
|
||||||
Debug( "Test ok, using format \"$prefix<command>$suffix\"\n" );
|
Debug("Test ok, using format \"$prefix<command>$suffix\"");
|
||||||
return( $prefix, $suffix );
|
return( $prefix, $suffix );
|
||||||
} else {
|
} else {
|
||||||
chomp($output);
|
chomp($output);
|
||||||
Debug( "Test failed, '$output'\n" );
|
Debug("Test failed, '$output'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Error( "Unable to find valid 'su' syntax\n" );
|
Error("Unable to find valid 'su' syntax");
|
||||||
exit( -1 );
|
exit -1;
|
||||||
}
|
} # end sub getCmdFormat
|
||||||
|
|
||||||
our $testedShellSyntax = 0;
|
our $testedShellSyntax = 0;
|
||||||
our ( $cmdPrefix, $cmdSuffix );
|
our ( $cmdPrefix, $cmdSuffix );
|
||||||
|
@ -161,23 +137,23 @@ sub runCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
my $command = shift;
|
my $command = shift;
|
||||||
$command = $Config{ZM_PATH_BIN}."/".$command;
|
$command = $Config{ZM_PATH_BIN}.'/'.$command;
|
||||||
if ( $cmdPrefix ) {
|
if ( $cmdPrefix ) {
|
||||||
$command = $cmdPrefix.$command.$cmdSuffix;
|
$command = $cmdPrefix.$command.$cmdSuffix;
|
||||||
}
|
}
|
||||||
Debug( "Command: $command\n" );
|
Debug("Command: $command");
|
||||||
my $output = qx($command);
|
my $output = qx($command);
|
||||||
my $status = $? >> 8;
|
my $status = $? >> 8;
|
||||||
chomp($output);
|
chomp($output);
|
||||||
if ( $status || logDebugging() ) {
|
if ( $status || logDebugging() ) {
|
||||||
if ( $status ) {
|
if ( $status ) {
|
||||||
Error( "Unable to run \"$command\", output is \"$output\", status is $status\n" );
|
Error("Unable to run \"$command\", output is \"$output\", status is $status");
|
||||||
} else {
|
} else {
|
||||||
Debug( "Output: $output\n" );
|
Debug("Output: $output");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return( $output );
|
return $output;
|
||||||
}
|
} # end sub runCommand
|
||||||
|
|
||||||
sub createEventPath {
|
sub createEventPath {
|
||||||
my $event = shift;
|
my $event = shift;
|
||||||
|
@ -210,7 +186,7 @@ sub _checkProcessOwner {
|
||||||
$_setFileOwner = 0;
|
$_setFileOwner = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return( $_setFileOwner );
|
return $_setFileOwner;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub setFileOwner {
|
sub setFileOwner {
|
||||||
|
@ -219,7 +195,7 @@ sub setFileOwner {
|
||||||
if ( _checkProcessOwner() ) {
|
if ( _checkProcessOwner() ) {
|
||||||
chown( $_ownerUid, $_ownerGid, $file )
|
chown( $_ownerUid, $_ownerGid, $file )
|
||||||
or Fatal( "Can't change ownership of file '$file' to '"
|
or Fatal( "Can't change ownership of file '$file' to '"
|
||||||
.$Config{ZM_WEB_USER}.":".$Config{ZM_WEB_GROUP}."': $!"
|
.$Config{ZM_WEB_USER}.':'.$Config{ZM_WEB_GROUP}."': $!"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -234,13 +210,13 @@ sub _checkForImageInfo {
|
||||||
};
|
};
|
||||||
$_hasImageInfo = $@?0:1;
|
$_hasImageInfo = $@?0:1;
|
||||||
}
|
}
|
||||||
return( $_hasImageInfo );
|
return $_hasImageInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub createEvent {
|
sub createEvent {
|
||||||
my $event = shift;
|
my $event = shift;
|
||||||
|
|
||||||
Debug( "Creating event" );
|
Debug('Creating event');
|
||||||
#print( Dumper( $event )."\n" );
|
#print( Dumper( $event )."\n" );
|
||||||
|
|
||||||
_checkForImageInfo();
|
_checkForImageInfo();
|
||||||
|
@ -561,38 +537,33 @@ __END__
|
||||||
|
|
||||||
=head1 NAME
|
=head1 NAME
|
||||||
|
|
||||||
ZoneMinder::Database - Perl extension for blah blah blah
|
ZoneMinder::General - Utility Functions for ZoneMinder
|
||||||
|
|
||||||
=head1 SYNOPSIS
|
=head1 SYNOPSIS
|
||||||
|
|
||||||
use ZoneMinder::Database;
|
use ZoneMinder::General;
|
||||||
blah blah blah
|
blah blah blah
|
||||||
|
|
||||||
=head1 DESCRIPTION
|
=head1 DESCRIPTION
|
||||||
|
|
||||||
Stub documentation for ZoneMinder, created by h2xs. It looks like the
|
This module contains the common definitions and functions used by the rest
|
||||||
author of the extension was negligent enough to leave the stub
|
of the ZoneMinder scripts
|
||||||
unedited.
|
|
||||||
|
|
||||||
Blah blah blah.
|
|
||||||
|
|
||||||
=head2 EXPORT
|
=head2 EXPORT
|
||||||
|
|
||||||
None by default.
|
functions => [ qw(
|
||||||
|
executeShellCommand
|
||||||
|
getCmdFormat
|
||||||
|
runCommand
|
||||||
|
setFileOwner
|
||||||
|
createEventPath
|
||||||
|
createEvent
|
||||||
|
makePath
|
||||||
|
jsonEncode
|
||||||
|
jsonDecode
|
||||||
|
) ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
=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.
|
|
||||||
|
|
||||||
=head1 AUTHOR
|
=head1 AUTHOR
|
||||||
|
|
||||||
Philip Coombes, E<lt>philip.coombes@zoneminder.comE<gt>
|
Philip Coombes, E<lt>philip.coombes@zoneminder.comE<gt>
|
||||||
|
@ -601,9 +572,18 @@ Philip Coombes, E<lt>philip.coombes@zoneminder.comE<gt>
|
||||||
|
|
||||||
Copyright (C) 2001-2008 Philip Coombes
|
Copyright (C) 2001-2008 Philip Coombes
|
||||||
|
|
||||||
This library is free software; you can redistribute it and/or modify
|
This program is free software; you can redistribute it and/or
|
||||||
it under the same terms as Perl itself, either Perl version 5.8.3 or,
|
modify it under the terms of the GNU General Public License
|
||||||
at your option, any later version of Perl 5 you may have available.
|
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.
|
||||||
|
|
||||||
=cut
|
=cut
|
||||||
|
|
|
@ -441,11 +441,11 @@ sub databaseLevel {
|
||||||
$databaseLevel = $this->limit($databaseLevel);
|
$databaseLevel = $this->limit($databaseLevel);
|
||||||
if ( $this->{databaseLevel} != $databaseLevel ) {
|
if ( $this->{databaseLevel} != $databaseLevel ) {
|
||||||
if ( ( $databaseLevel > NOLOG ) and ( $this->{databaseLevel} <= NOLOG ) ) {
|
if ( ( $databaseLevel > NOLOG ) and ( $this->{databaseLevel} <= NOLOG ) ) {
|
||||||
if ( !$this->{dbh} ) {
|
if ( ! ( $ZoneMinder::Database::dbh or ZoneMinder::Database::zmDbConnect() ) ) {
|
||||||
$this->{dbh} = ZoneMinder::Database::zmDbConnect();
|
Warning("Failed connecting to db. Not using database logging.");
|
||||||
|
$this->{databaseLevel} = NOLOG;
|
||||||
|
return NOLOG;
|
||||||
}
|
}
|
||||||
} elsif ( $databaseLevel <= NOLOG && $this->{databaseLevel} > NOLOG ) {
|
|
||||||
undef($this->{dbh});
|
|
||||||
}
|
}
|
||||||
$this->{databaseLevel} = $databaseLevel;
|
$this->{databaseLevel} = $databaseLevel;
|
||||||
}
|
}
|
||||||
|
@ -558,12 +558,12 @@ sub logPrint {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( $level <= $this->{databaseLevel} ) {
|
if ( $level <= $this->{databaseLevel} ) {
|
||||||
if ( ! ( $this->{dbh} and $this->{dbh}->ping() ) ) {
|
if ( ! ( $ZoneMinder::Database::dbh and $ZoneMinder::Database::dbh->ping() ) ) {
|
||||||
$this->{sth} = undef;
|
$this->{sth} = undef;
|
||||||
# Turn this off because zDbConnect will do logging calls.
|
# Turn this off because zDbConnect will do logging calls.
|
||||||
my $oldlevel = $this->{databaseLevel};
|
my $oldlevel = $this->{databaseLevel};
|
||||||
$this->{databaseLevel} = NOLOG;
|
$this->{databaseLevel} = NOLOG;
|
||||||
if ( ! ( $this->{dbh} = ZoneMinder::Database::zmDbConnect() ) ) {
|
if ( ! ZoneMinder::Database::zmDbConnect() ) {
|
||||||
#print(STDERR "Can't log to database: ");
|
#print(STDERR "Can't log to database: ");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -571,10 +571,10 @@ sub logPrint {
|
||||||
}
|
}
|
||||||
|
|
||||||
my $sql = 'INSERT INTO Logs ( TimeKey, Component, ServerId, Pid, Level, Code, Message, File, Line ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, NULL )';
|
my $sql = 'INSERT INTO Logs ( TimeKey, Component, ServerId, Pid, Level, Code, Message, File, Line ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, NULL )';
|
||||||
$this->{sth} = $this->{dbh}->prepare_cached($sql) if ! $this->{sth};
|
$this->{sth} = $ZoneMinder::Database::dbh->prepare_cached($sql) if ! $this->{sth};
|
||||||
if ( !$this->{sth} ) {
|
if ( !$this->{sth} ) {
|
||||||
$this->{databaseLevel} = NOLOG;
|
$this->{databaseLevel} = NOLOG;
|
||||||
Error("Can't prepare log entry '$sql': ".$this->{dbh}->errstr());
|
Error("Can't prepare log entry '$sql': ".$ZoneMinder::Database::dbh->errstr());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -590,7 +590,7 @@ sub logPrint {
|
||||||
);
|
);
|
||||||
if ( !$res ) {
|
if ( !$res ) {
|
||||||
$this->{databaseLevel} = NOLOG;
|
$this->{databaseLevel} = NOLOG;
|
||||||
Error("Can't execute log entry '$sql': ".$this->{dbh}->errstr());
|
Error("Can't execute log entry '$sql': ".$ZoneMinder::Database::dbh->errstr());
|
||||||
}
|
}
|
||||||
} # end if doing db logging
|
} # end if doing db logging
|
||||||
} # end if level < effectivelevel
|
} # end if level < effectivelevel
|
||||||
|
|
|
@ -384,7 +384,7 @@ MAIN: while( $loop ) {
|
||||||
Debug("Checking for Medium Scheme Events under $$Storage{Path}/$monitor_dir");
|
Debug("Checking for Medium Scheme Events under $$Storage{Path}/$monitor_dir");
|
||||||
{
|
{
|
||||||
my @event_dirs = glob("$monitor_dir/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/*");
|
my @event_dirs = glob("$monitor_dir/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/*");
|
||||||
Debug(qq`glob("$monitor_dir/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/*") returned ` . scalar @event_dirs . " entries." );
|
Debug(qq`glob("$monitor_dir/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/*") returned ` . scalar @event_dirs . ' entries.' );
|
||||||
foreach my $event_dir ( @event_dirs ) {
|
foreach my $event_dir ( @event_dirs ) {
|
||||||
if ( ! -d $event_dir ) {
|
if ( ! -d $event_dir ) {
|
||||||
Debug( "$event_dir is not a dir. Skipping" );
|
Debug( "$event_dir is not a dir. Skipping" );
|
||||||
|
@ -403,6 +403,7 @@ MAIN: while( $loop ) {
|
||||||
$$Event{RelativePath} = $event_dir;
|
$$Event{RelativePath} = $event_dir;
|
||||||
$Event->MonitorId( $monitor_dir );
|
$Event->MonitorId( $monitor_dir );
|
||||||
$Event->StorageId( $Storage->Id() );
|
$Event->StorageId( $Storage->Id() );
|
||||||
|
$Event->StartTime( POSIX::strftime('%Y-%m-%d %H:%M:%S', gmtime(time_of_youngest_file($$Event{Path})) ) );
|
||||||
} # end foreach event
|
} # end foreach event
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -428,7 +429,7 @@ MAIN: while( $loop ) {
|
||||||
} # end foreach event
|
} # end foreach event
|
||||||
chdir( $Storage->Path() );
|
chdir( $Storage->Path() );
|
||||||
} # if USE_DEEP_STORAGE
|
} # if USE_DEEP_STORAGE
|
||||||
Debug( 'Got '.int(keys(%$fs_events))." filesystem events for monitor $monitor_dir\n" );
|
Debug( 'Got '.int(keys(%$fs_events))." filesystem events for monitor $monitor_dir" );
|
||||||
|
|
||||||
delete_empty_subdirs($$Storage{Path}.'/'.$monitor_dir);
|
delete_empty_subdirs($$Storage{Path}.'/'.$monitor_dir);
|
||||||
} # end foreach monitor
|
} # end foreach monitor
|
||||||
|
@ -446,7 +447,7 @@ MAIN: while( $loop ) {
|
||||||
next;
|
next;
|
||||||
}
|
}
|
||||||
my @event_ids = keys %$fs_events;
|
my @event_ids = keys %$fs_events;
|
||||||
Debug("Have " .scalar @event_ids . " events for monitor $monitor_id");
|
Debug('Have ' .scalar @event_ids . " events for monitor $monitor_id");
|
||||||
|
|
||||||
foreach my $fs_event_id ( sort { $a <=> $b } keys %$fs_events ) {
|
foreach my $fs_event_id ( sort { $a <=> $b } keys %$fs_events ) {
|
||||||
|
|
||||||
|
@ -490,7 +491,11 @@ MAIN: while( $loop ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} # end foreach Storage Area
|
} # end foreach Storage Area
|
||||||
redo MAIN if ( $cleaned );
|
|
||||||
|
if ( $cleaned ) {
|
||||||
|
Debug("Events were deleted, starting again.");
|
||||||
|
redo MAIN;
|
||||||
|
}
|
||||||
|
|
||||||
$cleaned = 0;
|
$cleaned = 0;
|
||||||
my $deleteMonitorSql = 'DELETE LOW_PRIORITY FROM Monitors WHERE Id = ?';
|
my $deleteMonitorSql = 'DELETE LOW_PRIORITY FROM Monitors WHERE Id = ?';
|
||||||
|
@ -508,13 +513,18 @@ MAIN: while( $loop ) {
|
||||||
|
|
||||||
# Foreach database monitor and it's list of events.
|
# Foreach database monitor and it's list of events.
|
||||||
while ( my ( $db_monitor, $db_events ) = each(%$db_monitors) ) {
|
while ( my ( $db_monitor, $db_events ) = each(%$db_monitors) ) {
|
||||||
|
Debug("Checking db events for monitor $db_monitor");
|
||||||
|
if ( ! $db_events ) {
|
||||||
|
Debug("Skipping db events for $db_monitor because there are none");
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
# If we found the monitor in the file system
|
# If we found the monitor in the file system
|
||||||
if ( my $fs_events = $fs_monitors->{$db_monitor} ) {
|
my $fs_events = $fs_monitors->{$db_monitor};
|
||||||
next if ! $db_events;
|
|
||||||
|
|
||||||
while ( my ( $db_event, $age ) = each( %$db_events ) ) {
|
while ( my ( $db_event, $age ) = each( %$db_events ) ) {
|
||||||
if ( ! defined( $fs_events->{$db_event} ) ) {
|
if ( ! ($fs_events and defined( $fs_events->{$db_event} ) ) ) {
|
||||||
|
Debug("Don't have an fs event for $db_event");
|
||||||
my $Event = ZoneMinder::Event->find_one( Id=>$db_event );
|
my $Event = ZoneMinder::Event->find_one( Id=>$db_event );
|
||||||
if ( ! $Event ) {
|
if ( ! $Event ) {
|
||||||
Debug("Event $db_event is no longer in db. Filter probably deleted it while we were auditing.");
|
Debug("Event $db_event is no longer in db. Filter probably deleted it while we were auditing.");
|
||||||
|
@ -526,7 +536,7 @@ MAIN: while( $loop ) {
|
||||||
next;
|
next;
|
||||||
}
|
}
|
||||||
if ( ! $Event->StartTime() ) {
|
if ( ! $Event->StartTime() ) {
|
||||||
Info("Event $$Event{Id} has no start time. deleting it.");
|
Info("Event $$Event{Id} has no start time.");
|
||||||
if ( confirm() ) {
|
if ( confirm() ) {
|
||||||
$Event->delete();
|
$Event->delete();
|
||||||
$cleaned = 1;
|
$cleaned = 1;
|
||||||
|
@ -556,20 +566,35 @@ MAIN: while( $loop ) {
|
||||||
aud_print( "Database event '".$Event->Path()." monitor:$db_monitor event:$db_event' does not exist in filesystem but too young to delete age: $age > MIN $Config{ZM_AUDIT_MIN_AGE}.\n" );
|
aud_print( "Database event '".$Event->Path()." monitor:$db_monitor event:$db_event' does not exist in filesystem but too young to delete age: $age > MIN $Config{ZM_AUDIT_MIN_AGE}.\n" );
|
||||||
}
|
}
|
||||||
} # end if exists in filesystem
|
} # end if exists in filesystem
|
||||||
|
} else {
|
||||||
|
Debug("Found fs event for $db_event, $age at " . $$fs_events{$db_event}->Path());
|
||||||
|
my $Event = new ZoneMinder::Event( $db_event );
|
||||||
|
if ( ! $Event->check_for_in_filesystem() ) {
|
||||||
|
Warning("Not found at " . $Event->Path() . ' was found at ' . $$fs_events{$db_event}->Path() );
|
||||||
|
Warning($Event->to_string());
|
||||||
|
Warning($$fs_events{$db_event}->to_string());
|
||||||
|
if ( $$fs_events{$db_event}->Scheme() ne $Event->Scheme() ) {
|
||||||
|
Info("Updating scheme on event $$Event{Id} from $$Event{Scheme} to $$fs_events{$db_event}{Scheme}");
|
||||||
|
$Event->Scheme($$fs_events{$db_event}->Scheme());
|
||||||
|
}
|
||||||
|
if ( $$fs_events{$db_event}->StorageId() != $Event->StorageId() ) {
|
||||||
|
Info("Updating storage area on event $$Event{Id} from $$Event{StorageId} to $$fs_events{$db_event}{StorageId}");
|
||||||
|
$Event->StorageId($$fs_events{$db_event}->StorageId());
|
||||||
|
}
|
||||||
|
if ( $$fs_events{$db_event}->StartTime() ne $Event->StartTime() ) {
|
||||||
|
Info("Updating StartTime on event $$Event{Id} from $$Event{StartTime} to $$fs_events{$db_event}{StartTime}");
|
||||||
|
if ( $$Event{Scheme} eq 'Deep' ) {
|
||||||
|
$Event->StartTime($$fs_events{$db_event}->StartTime());
|
||||||
|
} else {
|
||||||
|
$Event->StartTime($$fs_events{$db_event}->StartTime());
|
||||||
|
}
|
||||||
|
$Event->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$Event->save();
|
||||||
|
}
|
||||||
} # end if ! in fs_events
|
} # end if ! in fs_events
|
||||||
} # foreach db_event
|
} # foreach db_event
|
||||||
#} else {
|
|
||||||
#my $Monitor = new ZoneMinder::Monitor( $db_monitor );
|
|
||||||
#my $Storage = $Monitor->Storage();
|
|
||||||
#aud_print( "Database monitor '$db_monitor' does not exist in filesystem, should have been at ".$Storage->Path().'/'.$Monitor->Id()."\n" );
|
|
||||||
#if ( confirm() )
|
|
||||||
#{
|
|
||||||
# We don't actually do this in case it's new
|
|
||||||
#my $res = $deleteMonitorSth->execute( $db_monitor )
|
|
||||||
# or Fatal( "Can't execute: ".$deleteMonitorSth->errstr() );
|
|
||||||
#$cleaned = 1;
|
|
||||||
#}
|
|
||||||
}
|
|
||||||
} # end foreach db_monitor
|
} # end foreach db_monitor
|
||||||
if ( $cleaned ) {
|
if ( $cleaned ) {
|
||||||
Debug("Have done some cleaning, restarting.");
|
Debug("Have done some cleaning, restarting.");
|
||||||
|
@ -954,7 +979,7 @@ sub delete_empty_directories {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
my @contents = map { ( $_ eq '.' or $_ eq '..' ) ? () : $_ } readdir( $DIR );
|
my @contents = map { ( $_ eq '.' or $_ eq '..' ) ? () : $_ } readdir( $DIR );
|
||||||
Debug("delete_empty_directories $_[0] has " . @contents .' entries:' . ( @contents <= 2 ? join(',',@contents) : '' ));
|
#Debug("delete_empty_directories $_[0] has " . @contents .' entries:' . ( @contents <= 2 ? join(',',@contents) : '' ));
|
||||||
my @dirs = map { -d $_[0].'/'.$_ ? $_ : () } @contents;
|
my @dirs = map { -d $_[0].'/'.$_ ? $_ : () } @contents;
|
||||||
if ( @dirs ) {
|
if ( @dirs ) {
|
||||||
Debug("Have " . @dirs . " dirs");
|
Debug("Have " . @dirs . " dirs");
|
||||||
|
@ -975,6 +1000,25 @@ sub delete_empty_directories {
|
||||||
}
|
}
|
||||||
} # end sub delete_empty_directories
|
} # end sub delete_empty_directories
|
||||||
|
|
||||||
|
sub time_of_youngest_file {
|
||||||
|
my $dir = shift;
|
||||||
|
|
||||||
|
if ( ! opendir(DIR, $dir) ) {
|
||||||
|
Error("Can't open directory '$dir': $!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
my $youngest = (stat($dir))[9];
|
||||||
|
Debug("stat of $dir is $youngest");
|
||||||
|
foreach my $file ( readdir( DIR ) ) {
|
||||||
|
next if $file =~ /^\./;
|
||||||
|
$_ = (stat($dir))[9];
|
||||||
|
$youngest = $_ if $_ and ( $_ < $youngest );
|
||||||
|
#Debug("stat of $dir is $_ < $youngest");
|
||||||
|
}
|
||||||
|
Debug("stat of $dir is $youngest");
|
||||||
|
return $youngest;
|
||||||
|
} # end sub time_of_youngest_file
|
||||||
|
|
||||||
1;
|
1;
|
||||||
__END__
|
__END__
|
||||||
|
|
||||||
|
|
|
@ -315,12 +315,14 @@ sub isActiveSanityCheck {
|
||||||
if ( $sth->rows != 1 ) {
|
if ( $sth->rows != 1 ) {
|
||||||
# PP - no row, or too many rows. Either case is an error
|
# PP - no row, or too many rows. Either case is an error
|
||||||
Info( 'Fixing States table - either no default state or duplicate default states' );
|
Info( 'Fixing States table - either no default state or duplicate default states' );
|
||||||
$sql = "DELETE FROM States WHERE Name='default'";
|
if ( $sth->rows ) {
|
||||||
|
$sql = q`DELETE FROM States WHERE Name='default'`;
|
||||||
$sth = $dbh->prepare_cached( $sql )
|
$sth = $dbh->prepare_cached( $sql )
|
||||||
or Fatal( "Can't prepare '$sql': ".$dbh->errstr() );
|
or Fatal( "Can't prepare '$sql': ".$dbh->errstr() );
|
||||||
$res = $sth->execute()
|
$res = $sth->execute()
|
||||||
or Fatal( "Can't execute: ".$sth->errstr() );
|
or Fatal( "Can't execute: ".$sth->errstr() );
|
||||||
$sql = q`"INSERT INTO States (Name,Definition,IsActive) VALUES ('default','','1');`;
|
}
|
||||||
|
$sql = q`INSERT INTO States (Name,Definition,IsActive) VALUES ('default','','1');`;
|
||||||
$sth = $dbh->prepare_cached($sql)
|
$sth = $dbh->prepare_cached($sql)
|
||||||
or Fatal("Can't prepare '$sql': ".$dbh->errstr());
|
or Fatal("Can't prepare '$sql': ".$dbh->errstr());
|
||||||
$res = $sth->execute()
|
$res = $sth->execute()
|
||||||
|
|
|
@ -2110,6 +2110,7 @@ Monitor *Monitor::Load(MYSQL_ROW dbrow, bool load_zones, Purpose purpose) {
|
||||||
Camera *camera = 0;
|
Camera *camera = 0;
|
||||||
if ( type == "Local" ) {
|
if ( type == "Local" ) {
|
||||||
|
|
||||||
|
#if ZM_HAS_V4L
|
||||||
int extras = (deinterlacing>>24)&0xff;
|
int extras = (deinterlacing>>24)&0xff;
|
||||||
|
|
||||||
camera = new LocalCamera(
|
camera = new LocalCamera(
|
||||||
|
@ -2132,6 +2133,9 @@ Monitor *Monitor::Load(MYSQL_ROW dbrow, bool load_zones, Purpose purpose) {
|
||||||
record_audio,
|
record_audio,
|
||||||
extras
|
extras
|
||||||
);
|
);
|
||||||
|
#else
|
||||||
|
Fatal("ZoneMinder not built with Local Camera support");
|
||||||
|
#endif
|
||||||
} else if ( type == "Remote" ) {
|
} else if ( type == "Remote" ) {
|
||||||
if ( protocol == "http" ) {
|
if ( protocol == "http" ) {
|
||||||
camera = new RemoteCameraHttp(
|
camera = new RemoteCameraHttp(
|
||||||
|
|
|
@ -337,7 +337,7 @@ void StreamBase::openComms() {
|
||||||
strncpy(loc_addr.sun_path, loc_sock_path, sizeof(loc_addr.sun_path));
|
strncpy(loc_addr.sun_path, loc_sock_path, sizeof(loc_addr.sun_path));
|
||||||
loc_addr.sun_family = AF_UNIX;
|
loc_addr.sun_family = AF_UNIX;
|
||||||
Debug(3, "Binding to %s", loc_sock_path);
|
Debug(3, "Binding to %s", loc_sock_path);
|
||||||
if ( bind(sd, (struct sockaddr *)&loc_addr, strlen(loc_addr.sun_path)+sizeof(loc_addr.sun_family)+1) < 0 ) {
|
if ( ::bind(sd, (struct sockaddr *)&loc_addr, strlen(loc_addr.sun_path)+sizeof(loc_addr.sun_family)+1) < 0 ) {
|
||||||
Fatal("Can't bind: %s", strerror(errno));
|
Fatal("Can't bind: %s", strerror(errno));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -164,14 +164,23 @@ switch ( $_REQUEST['task'] ) {
|
||||||
$where = array();
|
$where = array();
|
||||||
$values = array();
|
$values = array();
|
||||||
if ( $minTime ) {
|
if ( $minTime ) {
|
||||||
preg_match('/(.+)(\.\d+)/', $minTime, $matches);
|
Logger::Debug("MinTime: $minTime");
|
||||||
|
if ( preg_match('/(.+)(\.\d+)/', $minTime, $matches) ) {
|
||||||
|
# This handles sub second precision
|
||||||
$minTime = strtotime($matches[1]).$matches[2];
|
$minTime = strtotime($matches[1]).$matches[2];
|
||||||
|
Logger::Debug("MinTime: $minTime");
|
||||||
|
} else {
|
||||||
|
$minTime = strtotime($minTime);
|
||||||
|
}
|
||||||
$where[] = 'TimeKey >= ?';
|
$where[] = 'TimeKey >= ?';
|
||||||
$values[] = $minTime;
|
$values[] = $minTime;
|
||||||
}
|
}
|
||||||
if ( $maxTime ) {
|
if ( $maxTime ) {
|
||||||
preg_match('/(.+)(\.\d+)/', $maxTime, $matches);
|
if ( preg_match('/(.+)(\.\d+)/', $maxTime, $matches) ) {
|
||||||
$maxTime = strtotime($matches[1]).$matches[2];
|
$maxTime = strtotime($matches[1]).$matches[2];
|
||||||
|
} else {
|
||||||
|
$maxTime = strtotime($maxTime);
|
||||||
|
}
|
||||||
$where[] = 'TimeKey <= ?';
|
$where[] = 'TimeKey <= ?';
|
||||||
$values[] = $maxTime;
|
$values[] = $maxTime;
|
||||||
}
|
}
|
||||||
|
@ -209,8 +218,15 @@ switch ( $_REQUEST['task'] ) {
|
||||||
}
|
}
|
||||||
$exportKey = substr(md5(rand()),0,8);
|
$exportKey = substr(md5(rand()),0,8);
|
||||||
$exportFile = "zm-log.$exportExt";
|
$exportFile = "zm-log.$exportExt";
|
||||||
$exportPath = ZM_PATH_SWAP."/zm-log-$exportKey.$exportExt";
|
if ( ! file_exists(ZM_DIR_EXPORTS) ) {
|
||||||
if ( !($exportFP = fopen( $exportPath, "w" )) )
|
Logger::Debug('Creating ' . ZM_DIR_EXPORTS);
|
||||||
|
if ( ! mkdir(ZM_DIR_EXPORTS) ) {
|
||||||
|
Fatal("Can't create exports dir at '".ZM_DIR_EXPORTS."'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$exportPath = ZM_DIR_EXPORTS."/zm-log-$exportKey.$exportExt";
|
||||||
|
Logger::Debug("Exporting to $exportPath");
|
||||||
|
if ( !($exportFP = fopen($exportPath, 'w')) )
|
||||||
Fatal("Unable to open log export file $exportPath");
|
Fatal("Unable to open log export file $exportPath");
|
||||||
$logs = array();
|
$logs = array();
|
||||||
foreach ( dbFetchAll($sql, NULL, $values) as $log ) {
|
foreach ( dbFetchAll($sql, NULL, $values) as $log ) {
|
||||||
|
@ -218,6 +234,8 @@ switch ( $_REQUEST['task'] ) {
|
||||||
$log['Server'] = ( $log['ServerId'] and isset($servers_by_Id[$log['ServerId']]) ) ? $servers_by_Id[$log['ServerId']]->Name() : '';
|
$log['Server'] = ( $log['ServerId'] and isset($servers_by_Id[$log['ServerId']]) ) ? $servers_by_Id[$log['ServerId']]->Name() : '';
|
||||||
$logs[] = $log;
|
$logs[] = $log;
|
||||||
}
|
}
|
||||||
|
Logger::Debug(count($logs)." lines being exported by $sql " . implode(',',$values));
|
||||||
|
|
||||||
switch( $format ) {
|
switch( $format ) {
|
||||||
case 'text' :
|
case 'text' :
|
||||||
{
|
{
|
||||||
|
@ -390,7 +408,7 @@ switch ( $_REQUEST['task'] ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$exportFile = "zm-log.$exportExt";
|
$exportFile = "zm-log.$exportExt";
|
||||||
$exportPath = ZM_PATH_SWAP."/zm-log-$exportKey.$exportExt";
|
$exportPath = ZM_DIR_EXPORTS."/zm-log-$exportKey.$exportExt";
|
||||||
|
|
||||||
header('Pragma: public');
|
header('Pragma: public');
|
||||||
header('Expires: 0');
|
header('Expires: 0');
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
*/
|
*/
|
||||||
Configure::write('Error', array(
|
Configure::write('Error', array(
|
||||||
'handler' => 'ErrorHandler::handleError',
|
'handler' => 'ErrorHandler::handleError',
|
||||||
'level' => E_ALL & ~E_DEPRECATED,
|
'level' => E_ALL & ~E_DEPRECATED & ~E_NOTICE,
|
||||||
'trace' => true
|
'trace' => true
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -9,20 +9,95 @@ class Monitor {
|
||||||
private $defaults = array(
|
private $defaults = array(
|
||||||
'Id' => null,
|
'Id' => null,
|
||||||
'Name' => '',
|
'Name' => '',
|
||||||
'StorageId' => 0,
|
|
||||||
'ServerId' => 0,
|
'ServerId' => 0,
|
||||||
|
'StorageId' => 0,
|
||||||
'Type' => 'Ffmpeg',
|
'Type' => 'Ffmpeg',
|
||||||
'Function' => 'None',
|
'Function' => 'None',
|
||||||
'Enabled' => 1,
|
'Enabled' => 1,
|
||||||
'LinkedMonitors' => null,
|
'LinkedMonitors' => null,
|
||||||
|
'Triggers' => null,
|
||||||
|
'Device' => '',
|
||||||
|
'Channel' => 0,
|
||||||
|
'Format' => '0',
|
||||||
|
'V4LMultiBuffer' => null,
|
||||||
|
'V4LCapturesPerFrame' => null,
|
||||||
|
'Protocol' => null,
|
||||||
|
'Method' => '',
|
||||||
|
'Host' => null,
|
||||||
|
'Port' => '',
|
||||||
|
'SubPath' => '',
|
||||||
|
'Path' => null,
|
||||||
|
'Options' => null,
|
||||||
|
'User' => null,
|
||||||
|
'Pass' => null,
|
||||||
|
// These are NOT NULL default 0 in the db, but 0 is not a valid value. FIXME
|
||||||
'Width' => null,
|
'Width' => null,
|
||||||
'Height' => null,
|
'Height' => null,
|
||||||
|
'Colours' => 1,
|
||||||
|
'Palette' => '0',
|
||||||
'Orientation' => null,
|
'Orientation' => null,
|
||||||
|
'Deinterlacing' => 0,
|
||||||
|
'SaveJPEGs' => 3,
|
||||||
|
'VideoWriter' => '0',
|
||||||
|
'OutputCodec' => null,
|
||||||
|
'OutputContainer' => null,
|
||||||
|
'EncoderParameters' => null,
|
||||||
|
'RecordAudio' => 0,
|
||||||
|
'RTSPDescribe' => null,
|
||||||
|
'Brightness' => -1,
|
||||||
|
'Contrast' => -1,
|
||||||
|
'Hue' => -1,
|
||||||
|
'Colour' => -1,
|
||||||
|
'EventPrefix' => 'Event-',
|
||||||
|
'LabelFormat' => null,
|
||||||
|
'LabelX' => 0,
|
||||||
|
'LabelY' => 0,
|
||||||
|
'LabelSize' => 1,
|
||||||
|
'ImageBufferCount' => 100,
|
||||||
|
'WarmupCount' => 0,
|
||||||
|
'PreEventCount' => 0,
|
||||||
|
'PostEventCount' => 0,
|
||||||
|
'StreamReplayBuffer' => 0,
|
||||||
|
'AlarmFrameCount' => 1,
|
||||||
|
'SectionLength' => 600,
|
||||||
|
'FrameSkip' => 0,
|
||||||
'AnalysisFPSLimit' => null,
|
'AnalysisFPSLimit' => null,
|
||||||
'ZoneCount' => 0,
|
'AnalysisUpdateDelete' => 0,
|
||||||
'Triggers' => null,
|
|
||||||
'MaxFPS' => null,
|
'MaxFPS' => null,
|
||||||
'AlarmMaxFPS' => null,
|
'AlarmMaxFPS' => null,
|
||||||
|
'FPSReportIneterval' => 100,
|
||||||
|
'RefBlencPerc' => 6,
|
||||||
|
'AlarmRefBlendPerc' => 6,
|
||||||
|
'Controllable' => 0,
|
||||||
|
'ControlId' => null,
|
||||||
|
'ControlDevice' => null,
|
||||||
|
'ControlAddress' => null,
|
||||||
|
'AutoStopTimeout' => null,
|
||||||
|
'TrackMotion' => 0,
|
||||||
|
'TrackDelay' => null,
|
||||||
|
'ReturnLocation' => -1,
|
||||||
|
'ReturnDelay' => null,
|
||||||
|
'DefaultView' => 'Events',
|
||||||
|
'DefaultRate' => 100,
|
||||||
|
'DefaultScale' => 100,
|
||||||
|
'SignalCheckPoints' => 0,
|
||||||
|
'SignalCheckColour' => '#0000BE',
|
||||||
|
'WebColour' => 'red',
|
||||||
|
'Exif' => 0,
|
||||||
|
'Sequence' => null,
|
||||||
|
'TotalEvents' => null,
|
||||||
|
'TotalEventDiskSpace' => null,
|
||||||
|
'HourEvents' => null,
|
||||||
|
'HourEventDiskSpace' => null,
|
||||||
|
'DayEvents' => null,
|
||||||
|
'DayEventDiskSpace' => null,
|
||||||
|
'WeekEvents' => null,
|
||||||
|
'WeekEventDiskSpace' => null,
|
||||||
|
'MonthEvents' => null,
|
||||||
|
'MonthEventDiskSpace' => null,
|
||||||
|
'ArchivedEvents' => null,
|
||||||
|
'ArchivedEventDiskSpace' => null,
|
||||||
|
'ZoneCount' => 0,
|
||||||
'Refresh' => null,
|
'Refresh' => null,
|
||||||
);
|
);
|
||||||
private $status_fields = array(
|
private $status_fields = array(
|
||||||
|
|
|
@ -98,7 +98,14 @@ function dbLog( $sql, $update=false ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function dbError( $sql ) {
|
function dbError( $sql ) {
|
||||||
Error( "SQL-ERR '".$dbConn->errorInfo()."', statement was '".$sql."'" );
|
global $dbConn;
|
||||||
|
$error = $dbConn->errorInfo();
|
||||||
|
if ( ! $error[0] )
|
||||||
|
return '';
|
||||||
|
|
||||||
|
$message = "SQL-ERR '".implode("\n",$dbConn->errorInfo())."', statement was '".$sql."'";
|
||||||
|
Error($message);
|
||||||
|
return $message;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dbEscape( $string ) {
|
function dbEscape( $string ) {
|
||||||
|
@ -136,6 +143,10 @@ function dbQuery( $sql, $params=NULL ) {
|
||||||
Logger::Debug("SQL: $sql values:" . ($params?implode(',',$params):'') );
|
Logger::Debug("SQL: $sql values:" . ($params?implode(',',$params):'') );
|
||||||
}
|
}
|
||||||
$result = $dbConn->query($sql);
|
$result = $dbConn->query($sql);
|
||||||
|
if ( ! $result ) {
|
||||||
|
Error("SQL: Error preparing $sql: " . $pdo->errorInfo);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ( defined('ZM_DB_DEBUG') ) {
|
if ( defined('ZM_DB_DEBUG') ) {
|
||||||
if ( $params )
|
if ( $params )
|
||||||
|
|
|
@ -192,7 +192,7 @@ echo output_link_if_exists( array(
|
||||||
<script src="<?php echo cache_bust($skinJsFile) ?>"></script>
|
<script src="<?php echo cache_bust($skinJsFile) ?>"></script>
|
||||||
<script src="js/logger.js"></script>
|
<script src="js/logger.js"></script>
|
||||||
<?php
|
<?php
|
||||||
if ($basename == 'watch') {
|
if ($basename == 'watch' or $basename == 'log' ) {
|
||||||
// This is used in the log popup for the export function. Not sure if it's used anywhere else
|
// This is used in the log popup for the export function. Not sure if it's used anywhere else
|
||||||
?>
|
?>
|
||||||
<script type="text/javascript" src="js/overlay.js"></script>
|
<script type="text/javascript" src="js/overlay.js"></script>
|
||||||
|
|
|
@ -328,7 +328,7 @@ for( $monitor_i = 0; $monitor_i < count($displayMonitors); $monitor_i += 1 ) {
|
||||||
?>
|
?>
|
||||||
<td class="colMark">
|
<td class="colMark">
|
||||||
<input type="checkbox" name="markMids[]" value="<?php echo $monitor['Id'] ?>" onclick="setButtonStates( this )"<?php if ( !canEdit( 'Monitors' ) ) { ?> disabled="disabled"<?php } ?>/>
|
<input type="checkbox" name="markMids[]" value="<?php echo $monitor['Id'] ?>" onclick="setButtonStates( this )"<?php if ( !canEdit( 'Monitors' ) ) { ?> disabled="disabled"<?php } ?>/>
|
||||||
<span class="glyphicon glyphicon-sort"></span>
|
<span class="glyphicon glyphicon-sort" title="Click and drag to change order"></span>
|
||||||
</td>
|
</td>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,22 +119,22 @@ if ( ! $Event->Id() ) {
|
||||||
<div id="menuBar1">
|
<div id="menuBar1">
|
||||||
<div id="nameControl">
|
<div id="nameControl">
|
||||||
<input type="text" id="eventName" name="eventName" value="<?php echo validHtmlStr($Event->Name()) ?>" />
|
<input type="text" id="eventName" name="eventName" value="<?php echo validHtmlStr($Event->Name()) ?>" />
|
||||||
<button value="Rename" onclick="renameEvent()"<?php if ( !canEdit( 'Events' ) ) { ?> disabled="disabled"<?php } ?>>
|
<button value="Rename" type="button" onclick="renameEvent()"<?php if ( !canEdit('Events') ) { ?> disabled="disabled"<?php } ?>>
|
||||||
<?php echo translate('Rename') ?></button>
|
<?php echo translate('Rename') ?></button>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
if ( canEdit('Events') ) {
|
if ( canEdit('Events') ) {
|
||||||
?>
|
?>
|
||||||
<div id="deleteEvent"><button onclick="deleteEvent()"><?php echo translate('Delete') ?></button></div>
|
<div id="deleteEvent"><button type="button" onclick="deleteEvent()"><?php echo translate('Delete') ?></button></div>
|
||||||
<div id="editEvent"><button onclick="editEvent()"><?php echo translate('Edit') ?></button></div>
|
<div id="editEvent"><button type="button" onclick="editEvent()"><?php echo translate('Edit') ?></button></div>
|
||||||
<div id="archiveEvent"<?php echo $Event->Archived == 1 ? ' class="hidden"' : '' ?>><button onclick="archiveEvent()"><?php echo translate('Archive') ?></button></div>
|
<div id="archiveEvent"<?php echo $Event->Archived == 1 ? ' class="hidden"' : '' ?>><button type="button" onclick="archiveEvent()"><?php echo translate('Archive') ?></button></div>
|
||||||
<div id="unarchiveEvent"<?php echo $Event->Archived == 0 ? ' class="hidden"' : '' ?>><button onclick="unarchiveEvent()"><?php echo translate('Unarchive') ?></button></div>
|
<div id="unarchiveEvent"<?php echo $Event->Archived == 0 ? ' class="hidden"' : '' ?>><button type="button" onclick="unarchiveEvent()"><?php echo translate('Unarchive') ?></button></div>
|
||||||
<?php
|
<?php
|
||||||
} // end if can edit Events
|
} // end if can edit Events
|
||||||
?>
|
?>
|
||||||
<div id="framesEvent"><button onclick="showEventFrames()"><?php echo translate('Frames') ?></button></div>
|
<div id="framesEvent"><button type="button" onclick="showEventFrames()"><?php echo translate('Frames') ?></button></div>
|
||||||
<div id="streamEvent" class="hidden"><button onclick="showStream()"><?php echo translate('Stream') ?></button></div>
|
<div id="streamEvent" class="hidden"><button onclick="showStream()"><?php echo translate('Stream') ?></button></div>
|
||||||
<div id="stillsEvent"><button onclick="showStills()"><?php echo translate('Stills') ?></button></div>
|
<div id="stillsEvent"><button type="button" onclick="showStills()"><?php echo translate('Stills') ?></button></div>
|
||||||
<?php
|
<?php
|
||||||
if ( $Event->DefaultVideo() ) {
|
if ( $Event->DefaultVideo() ) {
|
||||||
?>
|
?>
|
||||||
|
@ -142,11 +142,11 @@ if ( canEdit('Events') ) {
|
||||||
<?php
|
<?php
|
||||||
} else {
|
} else {
|
||||||
?>
|
?>
|
||||||
<div id="videoEvent"><button onclick="videoEvent();"><?php echo translate('Video') ?></button></div>
|
<div id="videoEvent"><button type="button" onclick="videoEvent();"><?php echo translate('Video') ?></button></div>
|
||||||
<?php
|
<?php
|
||||||
} // end if Event->DefaultVideo
|
} // end if Event->DefaultVideo
|
||||||
?>
|
?>
|
||||||
<div id="exportEvent"><button onclick="exportEvent();"><?php echo translate('Export') ?></button></div>
|
<div id="exportEvent"><button type="button" onclick="exportEvent();"><?php echo translate('Export') ?></button></div>
|
||||||
<div id="replayControl"><label for="replayMode"><?php echo translate('Replay') ?></label><?php echo buildSelect( "replayMode", $replayModes, "changeReplayMode();" ); ?></div>
|
<div id="replayControl"><label for="replayMode"><?php echo translate('Replay') ?></label><?php echo buildSelect( "replayMode", $replayModes, "changeReplayMode();" ); ?></div>
|
||||||
<div id="scaleControl"><label for="scale"><?php echo translate('Scale') ?></label><?php echo buildSelect( "scale", $scales, "changeScale();" ); ?></div>
|
<div id="scaleControl"><label for="scale"><?php echo translate('Scale') ?></label><?php echo buildSelect( "scale", $scales, "changeScale();" ); ?></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -214,8 +214,8 @@ if ( ZM_WEB_STREAM_METHOD == 'mpeg' && ZM_MPEG_LIVE_FORMAT ) {
|
||||||
<div id="eventImageFrame">
|
<div id="eventImageFrame">
|
||||||
<img id="eventImage" src="graphics/transparent.png" alt=""/>
|
<img id="eventImage" src="graphics/transparent.png" alt=""/>
|
||||||
<div id="eventImageBar">
|
<div id="eventImageBar">
|
||||||
<div id="eventImageClose"><input type="button" value="<?php echo translate('Close') ?>" onclick="hideEventImage()"/></div>
|
<div id="eventImageClose"><button type="button" onclick="hideEventImage()"><?php echo translate('Close') ?></button></div>
|
||||||
<div id="eventImageStats" class="hidden"><input type="button" value="<?php echo translate('Stats') ?>" onclick="showFrameStats()"/></div>
|
<div id="eventImageStats" class="hidden"><button type="button" onclick="showFrameStats()"><?php echo translate('Stats') ?></button></div>
|
||||||
<div id="eventImageData"><?php echo translate('Frame') ?> <span id="eventImageNo"></span></div>
|
<div id="eventImageData"><?php echo translate('Frame') ?> <span id="eventImageNo"></span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue