diff --git a/.cirrus.yml b/.cirrus.yml index bb3a87f88c..66da4331b5 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -3,7 +3,7 @@ task: freebsd_instance: matrix: - image_family: freebsd-14-0 - - image_family: freebsd-13-2 + - image_family: freebsd-13-3 prepare_script: - pkg install -yq git cmake pkgconf jpeg-turbo mysql80-client ffmpeg libvncserver libjwt catch p5-DBI p5-DBD-mysql p5-Date-Manip p5-Test-LWP-UserAgent p5-Sys-Mmap v4l_compat diff --git a/.github/workflows/ci-bullseye.yml b/.github/workflows/ci-bullseye.yml index d60c96a647..5f6feb0f6f 100644 --- a/.github/workflows/ci-bullseye.yml +++ b/.github/workflows/ci-bullseye.yml @@ -53,8 +53,8 @@ jobs: - name: Prepare run: mkdir build - name: Configure - run: cd build && cmake --version && cmake .. -DBUILD_MAN=0 -DBUILD_TEST_SUITE=1 -DENABLE_WERROR=1 -DZM_CRYPTO_BACKEND=${{ matrix.crypto_backend }} -DZM_JWT_BACKEND=${{ matrix.jwt_backend }} + run: cd build && cmake --version && cmake .. -DBUILD_MAN=0 -DBUILD_TEST_SUITE=0 -DENABLE_WERROR=1 -DZM_CRYPTO_BACKEND=${{ matrix.crypto_backend }} -DZM_JWT_BACKEND=${{ matrix.jwt_backend }} - name: Build run: cd build && make -j3 | grep --line-buffered -Ev '^(cp lib\/|Installing.+\.pm)' && (exit ${PIPESTATUS[0]}) - - name: Run tests - run: cd build/tests && ./tests "~[notCI]" + #- name: Run tests + #run: cd build/tests && ./tests "~[notCI]" diff --git a/.github/workflows/ci-centos-8.yml b/.github/workflows/ci-centos-8.yml index 5d04f16694..7018e3f3c1 100644 --- a/.github/workflows/ci-centos-8.yml +++ b/.github/workflows/ci-centos-8.yml @@ -37,8 +37,8 @@ jobs: - name: Prepare run: mkdir build - name: Configure - run: cd build && cmake --version && cmake .. -DBUILD_MAN=0 -DBUILD_TEST_SUITE=1 -DENABLE_WERROR=1 -DZM_CRYPTO_BACKEND=${{ matrix.crypto_backend }} -DZM_JWT_BACKEND=${{ matrix.jwt_backend }} + run: cd build && cmake --version && cmake .. -DBUILD_MAN=0 -DBUILD_TEST_SUITE=0 -DENABLE_WERROR=1 -DZM_CRYPTO_BACKEND=${{ matrix.crypto_backend }} -DZM_JWT_BACKEND=${{ matrix.jwt_backend }} - name: Build run: cd build && make -j3 | grep --line-buffered -Ev '^(cp lib\/|Installing.+\.pm)' && (exit ${PIPESTATUS[0]}) - - name: Run tests - run: cd build/tests && ./tests "~[notCI]" + #- name: Run tests + #run: cd build/tests && ./tests "~[notCI]" diff --git a/.github/workflows/create-packages.yml b/.github/workflows/create-packages.yml index 1a5efc8ad8..346f7e9a9f 100644 --- a/.github/workflows/create-packages.yml +++ b/.github/workflows/create-packages.yml @@ -21,6 +21,9 @@ jobs: - os: ubuntu dist: jammy arch: x86_64 + - os: ubuntu + dist: noble + arch: x86_64 - os: debian dist: bookworm arch: x86_64 diff --git a/db/triggers.sql b/db/triggers.sql index 58f09bcbf3..37e11bbc5e 100644 --- a/db/triggers.sql +++ b/db/triggers.sql @@ -133,7 +133,7 @@ BEGIN UPDATE Storage SET DiskSpace = COALESCE(DiskSpace,0) + NEW.DiskSpace WHERE Storage.Id = NEW.StorageId; END IF; IF ( OLD.DiskSpace ) THEN - UPDATE Storage SET DiskSpace = GREATEST(COALESCE(DiskSpace,0) - OLD.DiskSpace,0) WHERE Storage.Id = OLD.StorageId; + UPDATE Storage SET DiskSpace = GREATEST(COALESCE(DiskSpace,0) - COALESCE(OLD.DiskSpace,0),0) WHERE Storage.Id = OLD.StorageId; END IF; END IF; @@ -172,7 +172,6 @@ BEGIN TotalEventDiskSpace = GREATEST(COALESCE(TotalEventDiskSpace,0) - COALESCE(OLD.DiskSpace,0) + COALESCE(NEW.DiskSpace,0),0) WHERE Event_Summaries.MonitorId=OLD.MonitorId; END IF; - END; // diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index 1022afbca4..65eaa96a35 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -309,6 +309,7 @@ CREATE TABLE `Filters` ( `EmailTo` TEXT, `EmailSubject` TEXT, `EmailBody` TEXT, + `EmailServer` TEXT, `EmailFormat` enum('Individual','Summary') NOT NULL default 'Individual', `AutoMessage` tinyint(3) unsigned NOT NULL default '0', `AutoExecute` tinyint(3) unsigned NOT NULL default '0', @@ -559,6 +560,7 @@ CREATE TABLE `Monitors` ( `Encoder` varchar(32), `OutputContainer` enum('auto','mp4','mkv','webm'), `EncoderParameters` TEXT, + `WallClockTimestamps` TINYINT NOT NULL DEFAULT '0', `RecordAudio` TINYINT NOT NULL DEFAULT '0', `RecordingSource` enum('Primary','Secondary','Both') NOT NULL DEFAULT 'Primary', `RTSPDescribe` tinyint(1) unsigned, diff --git a/db/zm_update-1.37.27.sql b/db/zm_update-1.37.27.sql index ecd10e5489..6993e36f40 100644 --- a/db/zm_update-1.37.27.sql +++ b/db/zm_update-1.37.27.sql @@ -113,12 +113,12 @@ PREPARE stmt FROM @s; EXECUTE stmt; REPLACE INTO Monitors_Permissions (UserId,Permission, MonitorId) - SELECT Id, 'Edit', SUBSTRING_INDEX(SUBSTRING_INDEX(t.MonitorIds, ',', n.n), ',', -1) value FROM Users t CROSS JOIN ( - SELECT a.N + b.N * 10 + 1 n FROM (SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a ,(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b ORDER BY n ) n WHERE t.Monitors='Edit' and t.MonitorIds != '' AND n.n <= 1 + (LENGTH(t.MonitorIds) - LENGTH(REPLACE(t.MonitorIds, ',', ''))) ORDER BY value; + SELECT Id, 'Edit', SUBSTRING_INDEX(SUBSTRING_INDEX(Users.MonitorIds, ',', n.n), ',', -1) value FROM Users CROSS JOIN ( + SELECT a.N + b.N * 10 + 1 n FROM (SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a ,(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b ORDER BY n ) n WHERE Users.Monitors='Edit' and Users.MonitorIds != '' AND n.n <= 1 + (LENGTH(Users.MonitorIds) - LENGTH(REPLACE(Users.MonitorIds, ',', ''))) ORDER BY value; REPLACE INTO Monitors_Permissions (UserId,Permission, MonitorId) - SELECT Id, 'View', SUBSTRING_INDEX(SUBSTRING_INDEX(t.MonitorIds, ',', n.n), ',', -1) value FROM Users t CROSS JOIN ( - SELECT a.N + b.N * 10 + 1 n FROM (SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a ,(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b ORDER BY n ) n WHERE t.Monitors!='Edit' and t.MonitorIds != '' AND n.n <= 1 + (LENGTH(t.MonitorIds) - LENGTH(REPLACE(t.MonitorIds, ',', ''))) ORDER BY value; + SELECT Id, 'View', SUBSTRING_INDEX(SUBSTRING_INDEX(Users.MonitorIds, ',', n.n), ',', -1) value FROM Users CROSS JOIN ( + SELECT a.N + b.N * 10 + 1 n FROM (SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a ,(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b ORDER BY n ) n WHERE Users.Monitors!='Edit' and Users.MonitorIds != '' AND n.n <= 1 + (LENGTH(Users.MonitorIds) - LENGTH(REPLACE(Users.MonitorIds, ',', ''))) ORDER BY value; DELETE FROM Monitors_Permissions WHERE MonitorID NOT IN (SELECT Id FROM Monitors); ALTER TABLE Monitors_Permissions ADD CONSTRAINT Monitors_Permissions_ibfk_1 FOREIGN KEY (`MonitorId`) REFERENCES `Monitors` (`Id`) ON DELETE CASCADE; diff --git a/db/zm_update-1.37.62.sql b/db/zm_update-1.37.62.sql new file mode 100644 index 0000000000..3e3e9d9295 --- /dev/null +++ b/db/zm_update-1.37.62.sql @@ -0,0 +1,20 @@ +-- +-- Update Monitors table to have WallClockTimestamps +-- + +SELECT 'Checking for WallClockTImestamps in Monitors'; +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Monitors' + AND table_schema = DATABASE() + AND column_name = 'WallClockTimestamps' + ) > 0, +"SELECT 'Column WallClockTimestamps already exists on Monitors'", +"ALTER TABLE Monitors ADD `WallClockTimestamps` TINYINT NOT NULL DEFAULT '0' AFTER `EncoderParameters`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + + diff --git a/db/zm_update-1.37.63.sql b/db/zm_update-1.37.63.sql new file mode 100644 index 0000000000..c526f3fe90 --- /dev/null +++ b/db/zm_update-1.37.63.sql @@ -0,0 +1,11 @@ +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Filters' + AND column_name = 'EmailServer' + ) > 0, +"SELECT 'Column EmailServer already exists in Filters'", +"ALTER TABLE `Filters` ADD `EmailServer` TEXT AFTER `EmailBody`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/distros/redhat/zoneminder.spec b/distros/redhat/zoneminder.spec index f792b80e15..f6ace411b8 100644 --- a/distros/redhat/zoneminder.spec +++ b/distros/redhat/zoneminder.spec @@ -9,7 +9,7 @@ %global ceb_version 1.0-zm # RtspServer is configured as a git submodule -%global rtspserver_commit eab32851421ffe54fec0229c3efc44c642bc8d46 +%global rtspserver_commit 055d81fe1293429e496b19104a9ed3360755a440 %global sslcert %{_sysconfdir}/pki/tls/certs/localhost.crt %global sslkey %{_sysconfdir}/pki/tls/private/localhost.key @@ -18,8 +18,8 @@ %global zmtargetdistro %{?rhel:el%{rhel}}%{!?rhel:fc%{fedora}} Name: zoneminder -Version: 1.37.60 -Release: 2%{?dist} +Version: 1.36.34 +Release: 1%{?dist} Summary: A camera monitoring and analysis tool Group: System Environment/Daemons # jQuery is under the MIT license: https://jquery.org/license/ @@ -40,7 +40,7 @@ Source3: https://github.com/ZoneMinder/RtspServer/archive/%{rtspserver_commit}.t %{?rhel:BuildRequires: epel-rpm-macros} BuildRequires: systemd-devel -BuildRequires: mariadb-connector-c-devel +BuildRequires: mariadb-devel BuildRequires: perl-podlators BuildRequires: polkit-devel BuildRequires: cmake @@ -75,7 +75,6 @@ BuildRequires: libv4l-devel BuildRequires: desktop-file-utils BuildRequires: gzip BuildRequires: zlib-devel -BuildRequires: gsoap-devel # ZoneMinder looks for and records the location of the ffmpeg binary during build BuildRequires: ffmpeg @@ -106,6 +105,7 @@ Requires: php-gd Requires: php-intl Requires: php-process Requires: php-json +Requires: cambozola Requires: php-pecl-apcu Requires: net-tools Requires: psmisc @@ -125,7 +125,6 @@ Requires: perl(LWP::Protocol::https) Requires: perl(Module::Load::Conditional) Requires: ca-certificates Requires: zip -Requires: gsoap %{?systemd_requires} Requires(post): %{_bindir}/gpasswd @@ -197,6 +196,7 @@ rm -rf ./dep/RtspServer mv -f RtspServer-%{rtspserver_commit} ./dep/RtspServer # Change the following default values +./utils/zmeditconfigdata.sh ZM_OPT_CAMBOZOLA yes ./utils/zmeditconfigdata.sh ZM_OPT_CONTROL yes ./utils/zmeditconfigdata.sh ZM_CHECK_FOR_UPDATES no @@ -208,7 +208,8 @@ mv -f RtspServer-%{rtspserver_commit} ./dep/RtspServer %cmake \ -DZM_WEB_USER="%{zmuid_final}" \ -DZM_WEB_GROUP="%{zmgid_final}" \ - -DZM_TARGET_DISTRO="%{zmtargetdistro}" + -DZM_TARGET_DISTRO="%{zmtargetdistro}" \ + . %cmake_build @@ -223,8 +224,6 @@ desktop-file-install \ # Remove unwanted files and folders find %{buildroot} \( -name .htaccess -or -name .editorconfig -or -name .packlist -or -name .git -or -name .gitignore -or -name .gitattributes -or -name .travis.yml \) -type f -delete > /dev/null 2>&1 || : -rm -rf %{buildroot}/usr/include -rm -rf %{buildroot}/usr/cmake # Recursively change shebang in all relevant scripts and set execute permission find %{buildroot}%{_datadir}/zoneminder/www/api \( -name cake -or -name cake.php \) -type f -exec sed -i 's\^#!/usr/bin/env bash$\#!%{_buildshell}\' {} \; -exec %{__chmod} 755 {} \; @@ -337,8 +336,7 @@ ln -sf %{_sysconfdir}/zm/www/zoneminder.nginx.conf %{_sysconfdir}/zm/www/zonemin %config(noreplace) %{_sysconfdir}/logrotate.d/zoneminder %{_unitdir}/zoneminder.service -%{_datadir}/polkit-1/actions/com.zoneminder.* -%{_datadir}/polkit-1/rules.d/com.zoneminder.arp-scan.rules +%{_datadir}/polkit-1/actions/com.zoneminder.systemctl.policy %{_bindir}/zmsystemctl.pl %{_bindir}/zmaudit.pl @@ -360,7 +358,6 @@ ln -sf %{_sysconfdir}/zm/www/zoneminder.nginx.conf %{_sysconfdir}/zm/www/zonemin %{_bindir}/zmonvif-trigger.pl %{_bindir}/zmstats.pl %{_bindir}/zmrecover.pl -%{_bindir}/zmeventtool.pl %{_bindir}/zm_rtsp_server %{perl_vendorlib}/ZoneMinder* @@ -417,6 +414,10 @@ ln -sf %{_sysconfdir}/zm/www/zoneminder.nginx.conf %{_sysconfdir}/zm/www/zonemin %dir %attr(755,nginx,nginx) %{_localstatedir}/log/zoneminder %changelog +* Fri Aug 16 2024 Andrew Bauer - 1.36.34-1 +- 1.36.34 release +- remove el7 support + * Sun Nov 12 2023 Jonathan Bennett - 1.37.47 - Specify folders to remove before packaging diff --git a/docs/faq.rst b/docs/faq.rst index 8d1800f5a2..e7b3e86fef 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -481,8 +481,8 @@ Why do I only see black screens with a timestamp when monitoring my camera? In the monitor windows where you see the black screen with a timestamp, select settings and enter the Brightness, Contrast, Hue, and Color settings reported for the device by ``zmu -d -q -v``. 32768 may be appropriate values to try for these settings. After saving the settings, select Settings again to confirm they saved successfully. -How do I repair the MySQL Database? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +How do I repair the MySQL/MariaDB Database? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There is two ways to go about this. In most cases you can run from the command prompt -> ``mysqlcheck --all-databases --auto-repair -p your_database_password -u your_databse_user`` @@ -491,9 +491,9 @@ If that does not work then you will have to make sure that ZoneMinder is stopped ``myisamchk --silent --force --fast --update-state -O key_buffer=64M -O sort_buffer=64M -O read_buffer=1M -O write_buffer=1M /var/lib/mysql/*/*.MYI`` -How do I repair the MySQL Database when the cli fails? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -In Ubuntu, the commands listed above do not seem to work. However, actually doing it by hand from within MySQL does. (But that is beyond the scope of this document) But that got me thinking... And phpmyadmin does work. Bring up a terminal. +How do I repair the MySQL/MariaDB Database when the cli fails? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +In Ubuntu, the commands listed above do not seem to work. However, actually doing it by hand from within MySQL/MariaDB does. (But that is beyond the scope of this document) But that got me thinking... And phpmyadmin does work. Bring up a terminal. ``sudo apt-get install phpmyadmin`` Now go to ``http://zoneminder_IP/`` and stop the ZM service. Continue to ``http://zoneminder_IP/phpmyadmin`` and select the zoneminder database. Select and tables marked 'in use' and pick the action 'repare' to fix. Restart the zoneminder service from the web browser. Remove or disable the phpmyadmin tool, as it is not always the most secure thing around, and opens your database wide to any skilled hacker. @@ -508,8 +508,8 @@ Any time you update a major version that ZoneMinder depends on, you need to reco Zoneminder doesn't start automatically on boot ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Check the list for log entries like "zmfix[766]: ERR [Can't connect to server: Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)] ". -What can happen is that zoneminder is started too quickly after Mysql and tries to contact the database server before it's ready. Zoneminder gets no answer and aborts. -August 2010 - Ubuntu upgrades seem to be leaving several systems in this state. One way around this is to add a delay to the zoneminder startup script allowing Mysql to finish starting. +What can happen is that zoneminder is started too quickly after MySQL/MariaDB and tries to contact the database server before it's ready. Zoneminder gets no answer and aborts. +August 2010 - Ubuntu upgrades seem to be leaving several systems in this state. One way around this is to add a delay to the zoneminder startup script allowing MySQL/MariaDB to finish starting. "Simply adding 'sleep 15' in the line above 'zmfix -a' in the /etc/init.d/zoneminder file fixed my ZoneMinder startup problems!" - credit to Pada. Remote Path setup for Panasonic and other Camera diff --git a/docs/index.rst b/docs/index.rst index 740e9ec26f..3dd5ae010d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,6 +7,7 @@ ZoneMinder Documentation installationguide/index userguide/index api + web_ui_api faq contributing @@ -23,6 +24,9 @@ If you are facing issues that are not covered in the documentation, please feel :doc:`api` Information on using the CakePHP based API for interfacing to ZoneMinder +:doc:`web_ui_api` + Information on interfacing to ZoneMinder's Web UI + :doc:`faq` Frequently Asked Questions diff --git a/docs/installationguide/debian.rst b/docs/installationguide/debian.rst index 2b2acdc77c..082ee2b2ba 100644 --- a/docs/installationguide/debian.rst +++ b/docs/installationguide/debian.rst @@ -39,7 +39,7 @@ Run the following commands. :: - sudo apt install mariadb-server + sudo apt install apache2 mariadb-server Switch into root user and create database and database user @@ -74,7 +74,7 @@ The backports repository is deactivated by default, so with the second line we e :: - sudo echo 'deb http://deb.debian.org/debian bookworm-backports main contrib' >> /etc/apt/sources.list + sudo bash -c "echo 'deb http://deb.debian.org/debian bookworm-backports main contrib' >> /etc/apt/sources.list" sudo apt update sudo apt -t bookworm-backports install zoneminder @@ -102,6 +102,7 @@ To make sure zoneminder can read the configuration file, run the following comma sudo systemctl reload apache2.service sudo systemctl restart zoneminder.service sudo systemctl status zoneminder.service + sudo systemctl enable zoneminder.service # start zoneminder automatically at boot If the zoneminder.service show to be active and without any errors, you should be able to access zoneminder at ``http://yourhostname/zm`` diff --git a/docs/installationguide/multiserver.rst b/docs/installationguide/multiserver.rst index e3306b6926..5c4615027a 100644 --- a/docs/installationguide/multiserver.rst +++ b/docs/installationguide/multiserver.rst @@ -23,13 +23,13 @@ New installs .. sidebar :: Note - For systemd based linux distros, inspect the zoneminder service file, typically found under /lib/systemd/system. Changes may be required for multiserver to function correctly. For example, the service file may check for a running instance of mysql or mariadb running locally on the server. This check will need to be removed. Rather than edit the service file directly, copy the service file to /etc/systemd/system and edit the file in that location. + For systemd based linux distros, inspect the zoneminder service file, typically found under /lib/systemd/system. Changes may be required for multiserver to function correctly. For example, the service file may check for a running instance of MySQL or MariaDB running locally on the server. This check will need to be removed. Rather than edit the service file directly, copy the service file to /etc/systemd/system and edit the file in that location. 2. On each ZoneMinder server, edit zm.conf. Find the ZM_DB_HOST variable and set it to the name or ip address of your Database Server. Find the ZM_SERVER_HOST and enter a name for this ZoneMinder server. Use a name easily recognizable by you. This name is not used by ZoneMinder for dns or any other form of network connectivity. 3. Copy the file /usr/share/zoneminder/db/zm_create.sql from one of the ZoneMinder Servers to the machine targeted as the Database Server. -4. Install mysql/mariadb server onto the Database Server. +4. Install MySQL/MariaDB server onto the Database Server. 5. It is advised to run "mysql_secure_installation" to help secure the server. diff --git a/docs/userguide/filterevents.rst b/docs/userguide/filterevents.rst index 50de9933b7..a458e8c1f0 100644 --- a/docs/userguide/filterevents.rst +++ b/docs/userguide/filterevents.rst @@ -65,6 +65,7 @@ Here is what the filter window looks like * %ED% Event description * %ET% Time of the event * %EL% Length of the event + * %ELOC% Location of event Latitude, Longitude * %EF% Number of frames in the event * %EFA% Number of alarm frames in the event * %EST% Total score of the event diff --git a/docs/web_ui_api.rst b/docs/web_ui_api.rst new file mode 100644 index 0000000000..2e429b2f33 --- /dev/null +++ b/docs/web_ui_api.rst @@ -0,0 +1,218 @@ + +WEB UI API +########## + +This document will provide information on interfacing with ZoneMinder's WEB UI ajax components. + +Overview +******** + +The ZoneMinder Web UI is written in PHP and various aspects of the web ui use AJAX calls to perform various tasks. + +These AJAX requests take the form of a GET (NOT POST!). It requires a query parameter called request. +In addition, if authentication is turned on, you will need to append either username and password, auth hash or jwt token to supply authentication in the request. See the REST API documentation for further information on authentication. + +Acceptable values for request are: + +* :ref:`add_monitors` +* :ref:`alarm` +* :ref:`console` +* :ref:`control` +* :ref:`controlcaps` +* :ref:`device` +* :ref:`devices` +* :ref:`event` +* :ref:`events` +* :ref:`frames` +* :ref:`log` +* :ref:`modal` +* :ref:`models` +* :ref:`reports` +* :ref:`shutdown` +* :ref:`snapshots` +* :ref:`stats` +* :ref:`status` +* :ref:`stream` +* :ref:`tags` +* :ref:`watch` +* :ref:`zone` + +(In all examples, replace 'server' with IP or hostname & port where ZoneMinder is running) + +.. _add_monitors: + +add_monitors +============ + +.. _alarm: + +alarm +===== + +.. _console: + +console +======= + +.. _control: + +control +======= + +Used for sending PTZ commands to monitors. + +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: + +``GET`` command to ``https://server/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`` + + +.. _controlcaps: + +controlcaps +=========== + +.. _device: + +device +====== + +.. _devices: + +devices +======= + +.. _event: + +event +====== + +Commands are passed using the "action" query parameter. Available values are: + +* addtag, +* archive, +* delete, +* :ref:`download` +* eventdetail, +* export, +* getselectedtags, +* removetag, +* rename, +* unarchive, +* video + +.. _download: + +download +*********** + + Parameters are: + + * exportFormat: + tar or zip. Defaults to zip. + * exportFileName + Defaults to 'Export'+connkey + * id or eids[] + Specify events by single id or an array of events ids. + * filter: + A url-encoded representation of a filter to use to get a list of eids. + * mergevents: + Whether to leave each event as a single mp4, or merge events for each monitor into a single mp4 for that monitor. + * connkey: + a seimi-unique value to uniquely identify this request from others. Typically ZM uses 6 decimal digits generated randomly. + + For example, a request could look like: + +:: + + curl http://server/zm/index.php?view=request&request=event&action=download&connkey=198605&exportVideo=1&mergeevents=1&eids%5B%5D=15433324&eids%5B%5D=15433318&exportFileName=zmDownload-198605&exportFormat=zip + + +On success, the response will look like: + +:: + + { + "result": "Ok", + "exportFile": "?view=archive&type=zip&file=zmDownload-198605.zip", + "exportFormat": "zip", + "connkey": "198605" + } + +You may then use the value in exportFile as a url to download the generated zip file. + +.. _events: + +events +******* + +Commands are passed using the task query parameter. Available values are: archive, unarchive, delete, query + +.. _frames: + +frames +====== + +.. _log: + +log +=== + +.. _modal: + +modal +===== + +.. _models: + +models +====== + +.. _reports: + +reports +======= + +.. _shutdown: + +shutdown +======== + +.. _snapshots: + +snapshots +========= + +.. _stats: + +stats +===== + +.. _status: + +status +====== + +.. _stream: + +stream +====== + +.. _tags: + +tags +==== + +.. _watch: + +watch +====== + +.. _zone: + +zone +==== + diff --git a/misc/MacVendors.json b/misc/MacVendors.json index 16f96729f7..06c0ef3287 100644 --- a/misc/MacVendors.json +++ b/misc/MacVendors.json @@ -27,5 +27,6 @@ "000b82": { "type": "Grandstream", "control": "Grandstream", "vendor": "Grandstream Networks Inc"}, "fcecda": { "type": "Ubiquiti", "control": "", "vendor": "Ubiquiti Networks Inc." }, "001f92": { "type": "Avigilon", "control": "", "vendor": "Avigilon Corporation" }, - "001885": { "type": "Avigilon", "control": "", "vendor": "Avigilon Corporation" } + "001885": { "type": "Avigilon", "control": "", "vendor": "Avigilon Corporation" }, + "f0a731": { "type": "TP-Link", "control": "", "vendor": "TP-LINK TECHNOLOGIES CO.,LTD." } } diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Control.pm b/scripts/ZoneMinder/lib/ZoneMinder/Control.pm index 0ffc7b60d8..0a12b4dbac 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Control.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Control.pm @@ -329,6 +329,43 @@ sub executeCommand { &{$self->{$command}}($self, $params); } +# Uses LWP get command and adds debugging +# if $$self{BaseURL} is defined then it will be prepended +sub get { + my $self = shift; + my $url = shift; + if (!$url) { + Error('No url specified in get'); + return; + } + $url = $$self{BaseURL}.'/'.$url if $$self{BaseURL}; + my $response = $self->{ua}->get($url); + Debug("Response from $url: ". $response->status_line . ' ' . $response->content); + return $response; +} + +sub put { + my $self = shift; + my $url = shift; + if (!$url) { + Error('No url specified in put'); + return; + } + $url = $$self{BaseURL}.'/'.$url if $$self{BaseURL}; + my $req = HTTP::Request->new(PUT => $url); + my $content = shift; + if ( defined($content) ) { + $req->content_type('application/x-www-form-urlencoded; charset=UTF-8'); + $req->content($content); + } + my $res = $self->{ua}->request($req); + if (!$res->is_success) { + Error($res->status_line); + } # end unless res->is_success + Debug('Response: '. $res->status_line . ' ' . $res->content); + return $res; +} # end sub put + sub printMsg { my $self = shift; my $msg = shift; @@ -337,6 +374,54 @@ sub printMsg { Debug($msg.'['.$msg_len.']'); } +sub credentials { + my $self = shift; + @$self{'username', 'password'} = @_; +} + +sub get_realm { + my $self = shift; + my $url = shift; + my $response = $self->get($url); + return 1 if $response->is_success(); + + if ($response->status_line() eq '401 Unauthorized' and defined $$self{username}) { + my $headers = $response->headers(); + foreach my $k ( keys %$headers ) { + Debug("Initial Header $k => $$headers{$k}"); + } + if ( $$headers{'www-authenticate'} ) { + foreach my $auth_header ( ref $$headers{'www-authenticate'} eq 'ARRAY' ? @{$$headers{'www-authenticate'}} : ($$headers{'www-authenticate'})) { + my ( $auth, $tokens ) = $auth_header =~ /^(\w+)\s+(.*)$/; + my %tokens = map { /(\w+)="?([^"]+)"?/i } split(', ', $tokens ); + if ( $tokens{realm} ) { + if ( $$self{realm} ne $tokens{realm} ) { + $$self{realm} = $tokens{realm}; + Debug("Changing REALM to $$self{realm}, $$self{host}:$$self{port}, $$self{realm}, $$self{username}, $$self{password}"); + $self->{ua}->credentials("$$self{host}:$$self{port}", $$self{realm}, $$self{username}, $$self{password}); + $response = $self->get($url); + if ( !$response->is_success() ) { + Debug('Authentication still failed after updating REALM' . $response->status_line); + $headers = $response->headers(); + foreach my $k ( keys %$headers ) { + Debug("Initial Header $k => $$headers{$k}\n"); + } # end foreach + } else { + return 1; + } + } else { + Error('Authentication failed, not a REALM problem'); + } + } else { + Debug('Failed to match realm in tokens'); + } # end if + } # end foreach auth header + } else { + debug('No headers line'); + } # end if headers + } # end if not authen +} # end sub get_realm + 1; __END__ diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm b/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm index 1443a3a2a7..dd06babe93 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm @@ -55,6 +55,7 @@ AutoEmail EmailTo EmailSubject EmailBody +EmailServer EmailFormat AutoMessage AutoExecute diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in index f0cd68af3a..1a58fd5dc3 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in @@ -148,6 +148,7 @@ our %mem_data = ( size => { type=>'uint32', seq=>$mem_seq++ }, last_write_index => { type=>'int32', seq=>$mem_seq++ }, last_read_index => { type=>'int32', seq=>$mem_seq++ }, + image_count => { type=>'int32', seq=>$mem_seq++ }, state => { type=>'uint32', seq=>$mem_seq++ }, capture_fps => { type=>'double', seq=>$mem_seq++ }, analysis_fps => { type=>'double', seq=>$mem_seq++ }, @@ -168,7 +169,7 @@ our %mem_data = ( signal => { type=>'uint8', seq=>$mem_seq++ }, format => { type=>'uint8', seq=>$mem_seq++ }, reserved1 => { type=>'uint8', seq=>$mem_seq++ }, - reserved2 => { type=>'uint8', seq=>$mem_seq++ }, + #reserved2 => { type=>'uint8', seq=>$mem_seq++ }, imagesize => { type=>'uint32', seq=>$mem_seq++ }, last_frame_score => { type=>'uint32', seq=>$mem_seq++ }, audio_frequency => { type=>'uint32', seq=>$mem_seq++ }, diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm b/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm index 5534014cb4..56787c8404 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm @@ -292,7 +292,7 @@ $fields{model} = undef; Longitude => undef, RTSPStreamName => '', RTSPServer => 0, - Importance => 0, + Importance => 'Normal', MQTT_Enabled => 0, MQTT_Subscriptions => q`''`, ); diff --git a/scripts/zmaudit.pl.in b/scripts/zmaudit.pl.in index 1995c16ffd..aefd94b106 100644 --- a/scripts/zmaudit.pl.in +++ b/scripts/zmaudit.pl.in @@ -841,9 +841,11 @@ FROM `Frames` WHERE `EventId`=?'; unlink( split( /;/, $untainted_old_files ) ); } - # Now delete any old swap files - ( my $swap_image_root ) = ( $Config{ZM_PATH_SWAP} =~ /^(.*)$/ ); # De-taint - File::Find::find( { wanted=>\&deleteSwapImage, untaint=>1 }, $swap_image_root ); + eval { + # Now delete any old swap files + ( my $swap_image_root ) = ( $Config{ZM_PATH_SWAP} =~ /^(.*)$/ ); # De-taint + File::Find::find( { wanted=>\&deleteSwapImage, untaint=>1 }, $swap_image_root ); + }; # Prune the Logs table if required if ( $Config{ZM_LOG_DATABASE_LIMIT} ) { diff --git a/scripts/zmfilter.pl.in b/scripts/zmfilter.pl.in index 5e3d645d7b..7f24824bb5 100644 --- a/scripts/zmfilter.pl.in +++ b/scripts/zmfilter.pl.in @@ -119,23 +119,7 @@ if ( $Config{ZM_OPT_UPLOAD} ) { } } -if ( $Config{ZM_OPT_EMAIL} ) { - if ( $Config{ZM_NEW_MAIL_MODULES} ) { - require MIME::Lite; - require Net::SMTP; - } else { - require MIME::Entity; - } -} -if ( $Config{ZM_OPT_MESSAGE} ) { - if ( $Config{ZM_NEW_MAIL_MODULES} ) { - require MIME::Lite; - require Net::SMTP; - } else { - require MIME::Entity; - } -} $| = 1; @@ -648,6 +632,7 @@ sub substituteTags { my $filter = shift; my $Event = @_ ? shift : undef; my $attachments_ref = shift if @_; + my $options = @_ ? shift : {}; # First we'd better check what we need to get # We have a filter and an event, do we need any more @@ -658,7 +643,7 @@ sub substituteTags { my $Monitor = $Event->Monitor() if $Event and $need_monitor; my $Summary = $Monitor->Event_Summary() if $Monitor and $need_summary; - my $html = ($text =~ /{Name}/g; $text =~ s/%EC%/$Event->{Cause}/g; $text =~ s/%ED%/$Event->{Notes}/g; + $text =~ s/%ELOC%/$Event->{Latitude}, $Event->{Longitude}/g; $text =~ s/%ET%/$Event->{StartDateTime}/g; $text =~ s/%EVF%/$$Event{DefaultVideo}/g; # Event video filename $text =~ s/%EL%/$Event->{Length}/g; @@ -913,15 +899,16 @@ sub sendSummaryEmail { my $subject = substituteTags($$filter{EmailSubject}, $filter); return 0 if !$subject; + my $html = $$filter{EmailBody} =~ /$html}); foreach my $event (@events) { - $body .= substituteTags($summary_part, $filter, $event, \@attachments); + $body .= substituteTags($summary_part, $filter, $event, \@attachments, {html=>$html}); } return 0 if !$body; $body .= $body_tail; @@ -969,6 +956,8 @@ sub sendTheEmail { eval { if ($Config{ZM_NEW_MAIL_MODULES}) { + require MIME::Lite; + require Net::SMTP; my $total_size = 0; # Create the multipart container my $mail = MIME::Lite->new( @@ -1033,17 +1022,18 @@ sub sendTheEmail { } if (!$ssmtp_location) { Warning('Unable to find ssmtp, trying MIME::Lite->send'); - MIME::Lite->send('smtp', $Config{ZM_EMAIL_HOST}, Timeout=>60); + MIME::Lite->send('smtp', ($$filter{EmailServer} ? $$filter{EmailServer} : $Config{ZM_EMAIL_HOST}), Timeout=>60); $mail->send(); } else { ### Send using SSMTP $mail->send('sendmail', $ssmtp_location, $$filter{EmailTo}); } } else { - MIME::Lite->send('smtp', $Config{ZM_EMAIL_HOST}, Timeout=>60); + MIME::Lite->send('smtp', ($$filter{EmailServer} ? $$filter{EmailServer} : $Config{ZM_EMAIL_HOST}), Timeout=>60); $mail->send(); } } else { + require MIME::Entity; my $total_size = 0; my $mail = MIME::Entity->build( From => $Config{ZM_FROM_EMAIL}, @@ -1102,7 +1092,9 @@ sub sendTheEmail { if ( $total_size > 10*1024*1024 ) { Warning('Emails larger than 10Mb will often not be delivered! This one is '.int($total_size/(1024*1024)).'Mb'); } - $mail->smtpsend(Host => $Config{ZM_EMAIL_HOST}, MailFrom => $Config{ZM_FROM_EMAIL}); + $mail->smtpsend( + Host => ($$filter{EmailServer} ? $$filter{EmailServer} :$Config{ZM_EMAIL_HOST}), + MailFrom => $Config{ZM_FROM_EMAIL}); } }; if ( $@ ) { @@ -1140,6 +1132,8 @@ sub sendMessage { eval { if ( $Config{ZM_NEW_MAIL_MODULES} ) { + require MIME::Lite; + require Net::SMTP; ### Create the multipart container my $mail = MIME::Lite->new( From => $Config{ZM_FROM_EMAIL}, @@ -1184,6 +1178,7 @@ sub sendMessage { $mail->send(smtp=>$Config{ZM_EMAIL_HOST}, Timeout=>60); } } else { + require MIME::Entity; my $mail = MIME::Entity->build( From => $Config{ZM_FROM_EMAIL}, To => $Config{ZM_MESSAGE_ADDRESS}, diff --git a/scripts/zmstats.pl.in b/scripts/zmstats.pl.in index 93f6a93317..09f2e00faa 100644 --- a/scripts/zmstats.pl.in +++ b/scripts/zmstats.pl.in @@ -79,8 +79,8 @@ while (!$zm_terminate) { Debug("Deleted $rows Server Stats table entries by time"); } - # Clear out statuses for Monitors that have been set to None but for some reason didn't update the db - zmDbDo('DELETE FROM Monitor_Status WHERE MonitorId IN (SELECT Id FROM Monitors WHERE Capturing=\'None\')'); + # Clear out statuses for Monitors that aren't updating themselves. + zmDbDo('DELETE FROM Monitor_Status WHERE UpdatedOn < timestamp(DATE_SUB(NOW(), INTERVAL 1 MINUTE))'); zmDbDo('DELETE FROM Events_Hour WHERE StartDateTime < DATE_SUB(NOW(), INTERVAL 1 hour)'); zmDbDo('DELETE FROM Events_Day WHERE StartDateTime < DATE_SUB(NOW(), INTERVAL 1 day)'); diff --git a/scripts/zmupdate.pl.in b/scripts/zmupdate.pl.in index a35bf5b771..3c9aa04f91 100644 --- a/scripts/zmupdate.pl.in +++ b/scripts/zmupdate.pl.in @@ -941,7 +941,7 @@ if ( $version ) { foreach my $patch ( @files ) { my ( $v ) = $patch =~ /^zm_update\-([\d\.]+)\.sql$/; #PP make sure we use version compare - if ( version->parse('v'.$v) > version->parse('v'.$version) ) { + if ( version->parse('v'.$v) >= version->parse('v'.$version) ) { print("Upgrading DB to $v from $version\n"); if ( patchDB($dbh, $v) ) { my $res = $sth->execute($version) or die( "Can't execute: ".$sth->errstr() ); diff --git a/scripts/zmwatch.pl.in b/scripts/zmwatch.pl.in index 2651c96547..2d37ff475c 100644 --- a/scripts/zmwatch.pl.in +++ b/scripts/zmwatch.pl.in @@ -127,7 +127,9 @@ while (!$zm_terminate) { if (!$capture_time) { # We can't get the last capture time so can't be sure it's died, it might just be starting up. my $startup_time = zmGetStartupTime($monitor); - if (($now - $startup_time) > $Config{ZM_WATCH_MAX_DELAY}) { + my $startup_elapsed = $now - $startup_time; + if ($startup_elapsed > $Config{ZM_WATCH_MAX_DELAY}) { + Debug("Monitor $monitor->{Id} $monitor->{Name}, startup time $now - $startup_time $startup_elapsed ControlId()) { my $control = $monitor->Control(); if ($control and $control->CanReboot() and $control->open()) { diff --git a/src/zm_db.cpp b/src/zm_db.cpp index 788fc7e639..5703940232 100644 --- a/src/zm_db.cpp +++ b/src/zm_db.cpp @@ -196,14 +196,13 @@ int zmDbDo(const std::string &query) { if (!zmDbConnected and !zmDbConnect()) return 0; int rc; - while ((rc = mysql_query(&dbconn, query.c_str())) and !zm_terminate) { - Logger *logger = Logger::fetch(); - Logger::Level oldLevel = logger->databaseLevel(); - logger->databaseLevel(Logger::NOLOG); + Logger *logger = Logger::fetch(); + Logger::Level oldLevel = logger->databaseLevel(); + logger->databaseLevel(Logger::NOLOG); + while ((rc = mysql_query(&dbconn, query.c_str())) and !zm_terminate) { std::string reason = mysql_error(&dbconn); Debug(1, "Failed running sql query %s, thread_id: %lu, %d %s", query.c_str(), db_thread_id, rc, reason.c_str()); - logger->databaseLevel(oldLevel); if (mysql_ping(&dbconn)) { // Was a connection error @@ -222,6 +221,7 @@ int zmDbDo(const std::string &query) { } Debug(1, "Success running sql query %s, thread_id: %lu", query.c_str(), db_thread_id); + logger->databaseLevel(oldLevel); return 1; } diff --git a/src/zm_event.cpp b/src/zm_event.cpp index e77d1272d9..821c2ce7c0 100644 --- a/src/zm_event.cpp +++ b/src/zm_event.cpp @@ -42,12 +42,14 @@ Event::PreAlarmData Event::pre_alarm_data[MAX_PRE_ALARM_FRAMES] = {}; Event::Event( Monitor *p_monitor, + packetqueue_iterator *p_packetqueue_it, SystemTimePoint p_start_time, const std::string &p_cause, const StringSetMap &p_noteSetMap ) : id(0), monitor(p_monitor), + packetqueue_it(p_packetqueue_it), start_time(p_start_time), end_time(p_start_time), cause(p_cause), @@ -74,6 +76,8 @@ Event::Event( SystemTimePoint now = std::chrono::system_clock::now(); + packetqueue = monitor->GetPacketQueue(); + if (start_time.time_since_epoch() == Seconds(0)) { Warning("Event has zero time, setting to now"); end_time = start_time = now; @@ -144,10 +148,13 @@ Event::Event( Event::~Event() { Stop(); + if (thread_.joinable()) { + Debug(1, "Joining event thread"); // Should be. Issuing the stop and then getting the lock thread_.join(); } + packetqueue->free_it(packetqueue_it); /* Close the video file */ // We close the videowriter first, because if we finish the event, we might try to view the file, but we aren't done writing it yet. @@ -237,29 +244,23 @@ bool Event::WriteFrameImage(Image *image, SystemTimePoint timestamp, const char (alarm_frame && (config.jpeg_alarm_file_quality > config.jpeg_file_quality)) ? config.jpeg_alarm_file_quality : 0; // quality to use, zero is default - bool rc; - SystemTimePoint jpeg_timestamp = monitor->Exif() ? timestamp : SystemTimePoint(); if (!config.timestamp_on_capture) { // stash the image we plan to use in another pointer regardless if timestamped. // exif is only timestamp at present this switches on or off for write - Image *ts_image = new Image(*image); - monitor->TimestampImage(ts_image, timestamp); - rc = ts_image->WriteJpeg(event_file, thisquality, jpeg_timestamp); - delete ts_image; - } else { - rc = image->WriteJpeg(event_file, thisquality, jpeg_timestamp); + Image ts_image(*image); + monitor->TimestampImage(&ts_image, timestamp); + return ts_image.WriteJpeg(event_file, thisquality, jpeg_timestamp); } - - return rc; + return image->WriteJpeg(event_file, thisquality, jpeg_timestamp); } -bool Event::WritePacket(const std::shared_ptr&packet) { +bool Event::WritePacket(const std::shared_ptrpacket) { if (videoStore->writePacket(packet) < 0) return false; return true; -} // bool Event::WriteFrameVideo +} // bool Event::WritePacket void Event::updateNotes(const StringSetMap &newNoteSetMap) { bool update = false; @@ -313,16 +314,7 @@ void Event::updateNotes(const StringSetMap &newNoteSetMap) { } // end if update } // void Event::updateNotes(const StringSetMap &newNoteSetMap) -void Event::AddPacket(const std::shared_ptr&packet) { - { - std::unique_lock lck(packet_queue_mutex); - - packet_queue.push(std::move(packet)); - } - packet_queue_condition.notify_one(); -} - -void Event::AddPacket_(const std::shared_ptr&packet) { +void Event::AddPacket_(const std::shared_ptrpacket) { have_video_keyframe = have_video_keyframe || ( ( packet->codec_type == AVMEDIA_TYPE_VIDEO ) && ( packet->keyframe || monitor->GetOptVideoWriter() == Monitor::ENCODE) ); @@ -692,25 +684,45 @@ void Event::Run() { // The idea is to process the queue no matter what so that all packets get processed. // We only break if the queue is empty - while (true) { - std::shared_ptr packet = nullptr; - { - std::unique_lock lck(packet_queue_mutex); - - if (packet_queue.empty()) { - if (terminate_ or zm_terminate) break; - packet_queue_condition.wait(lck); - // Necessary because we don't hold the lock in the while condition - } - if (!packet_queue.empty()) { - packet = packet_queue.front(); - packet_queue.pop(); + while (!terminate_ and !zm_terminate) { + ZMLockedPacket *packet_lock = packetqueue->get_packet_no_wait(packetqueue_it); + if (packet_lock) { + std::shared_ptr packet = packet_lock->packet_; + if (!packet->decoded) { + delete packet_lock; + // Stay behind decoder + Microseconds sleep_for = Microseconds(ZM_SAMPLE_RATE); + Debug(4, "Sleeping for %" PRId64 "us", int64(sleep_for.count())); + std::this_thread::sleep_for(sleep_for); + continue; } - } // end lock scope - if (packet) { + Debug(1, "Adding packet %d", packet->image_index); this->AddPacket_(packet); - } + + if (packet->image) { + if (monitor->GetOptVideoWriter() == Monitor::PASSTHROUGH) { + if (!save_jpegs) { + Debug(1, "Deleting image data for %d", packet->image_index); + // Don't need raw images anymore + delete packet->image; + packet->image = nullptr; + } + } + if (packet->analysis_image and !(save_jpegs & 2)) { + Debug(1, "Deleting analysis image data for %d", packet->image_index); + delete packet->analysis_image; + packet->analysis_image = nullptr; + } + } // end if packet->image + Debug(1, "Deleting packet lock"); + delete packet_lock; + // Important not to increment it until after we are done with the packet because clearPackets checks for iterators pointing to it. + packetqueue->increment_it(packetqueue_it); + } else { + if (terminate_ or zm_terminate) return; + usleep(10000); + } // end if packet_lock } // end while } // end Run() diff --git a/src/zm_event.h b/src/zm_event.h index bc897403d2..0a7a868aa5 100644 --- a/src/zm_event.h +++ b/src/zm_event.h @@ -23,6 +23,7 @@ #include "zm_config.h" #include "zm_define.h" #include "zm_packet.h" +#include "zm_packetqueue.h" #include "zm_storage.h" #include "zm_time.h" #include "zm_utils.h" @@ -77,6 +78,8 @@ class Event { uint64_t id; Monitor *monitor; + PacketQueue * packetqueue; + packetqueue_iterator * packetqueue_it; SystemTimePoint start_time; SystemTimePoint end_time; std::string cause; @@ -106,10 +109,6 @@ class Event { void createNotes(std::string ¬es); - std::queue> packet_queue; - std::mutex packet_queue_mutex; - std::condition_variable packet_queue_condition; - void Run(); std::atomic terminate_; @@ -120,9 +119,10 @@ class Event { static bool ValidateFrameSocket(int); Event(Monitor *p_monitor, - SystemTimePoint p_start_time, - const std::string &p_cause, - const StringSetMap &p_noteSetMap); + packetqueue_iterator * p_packetqueue_it, + SystemTimePoint p_start_time, + const std::string &p_cause, + const StringSetMap &p_noteSetMap); ~Event(); uint64_t Id() const { return id; } @@ -135,9 +135,8 @@ class Event { SystemTimePoint EndTime() const { return end_time; } TimePoint::duration Duration() const { return end_time - start_time; }; - void AddPacket(const std::shared_ptr &p); - void AddPacket_(const std::shared_ptr &p); - bool WritePacket(const std::shared_ptr &p); + void AddPacket_(const std::shared_ptr p); + bool WritePacket(const std::shared_ptr p); bool SendFrameImage(const Image *image, bool alarm_frame=false); bool WriteFrameImage(Image *image, SystemTimePoint timestamp, const char *event_file, bool alarm_frame = false) const; @@ -146,11 +145,7 @@ class Event { void AddFrame(const std::shared_ptr&packet); void Stop() { - { - std::unique_lock lck(packet_queue_mutex); - terminate_ = true; - } - packet_queue_condition.notify_all(); + terminate_ = true; } bool Stopped() const { return terminate_; } diff --git a/src/zm_eventstream.cpp b/src/zm_eventstream.cpp index 82c8f96431..a251c0d03c 100644 --- a/src/zm_eventstream.cpp +++ b/src/zm_eventstream.cpp @@ -43,7 +43,7 @@ constexpr Milliseconds EventStream::STREAM_PAUSE_WAIT; bool EventStream::loadInitialEventData(int monitor_id, SystemTimePoint event_time) { std::string sql = stringtf("SELECT `Id` FROM `Events` WHERE " - "`MonitorId` = %d AND unix_timestamp(`EndDateTime`) > %ld " + "`MonitorId` = %d AND unix_timestamp(`EndDateTime`) > %jd " "ORDER BY `Id` ASC LIMIT 1", monitor_id, std::chrono::system_clock::to_time_t(event_time)); MYSQL_RES *result = zmDbFetch(sql); @@ -874,8 +874,10 @@ bool EventStream::sendFrame(Microseconds delta_us) { int img_buffer_size = 0; uint8_t *img_buffer = temp_img_buffer; - fprintf(stdout, "--" BOUNDARY "\r\n"); + if (type != STREAM_SINGLE) + fprintf(stdout, "--" BOUNDARY "\r\n"); switch ( type ) { + case STREAM_SINGLE : case STREAM_JPEG : send_image->EncodeJpeg(img_buffer, &img_buffer_size); fputs("Content-Type: image/jpeg\r\n", stdout); @@ -940,12 +942,13 @@ void EventStream::runStream() { Microseconds delta = Microseconds(0); while (!zm_terminate) { - start = std::chrono::steady_clock::now(); + now = start = std::chrono::steady_clock::now(); { std::scoped_lock lck{mutex}; send_frame = false; + TimePoint::duration time_since_last_send = now - last_frame_sent; if (!paused) { // Figure out if we should send this frame @@ -964,16 +967,22 @@ void EventStream::runStream() { send_frame = true; } else if (!send_frame) { // We are paused, not stepping and doing nothing, meaning that comms didn't set send_frame to true - if (now - last_frame_sent > MAX_STREAM_DELAY) { + if (time_since_last_send > MAX_STREAM_DELAY) { // Send keepalive Debug(2, "Sending keepalive frame"); send_frame = true; + } else { + Debug(4, "Not Sending keepalive frame now %.2f - %.2f last = %.2f > Max %.2f", + FPSeconds(now.time_since_epoch()).count(), + FPSeconds(last_frame_sent.time_since_epoch()).count(), + FPSeconds(time_since_last_send).count(), + FPSeconds(MAX_STREAM_DELAY).count() + ); } } // end if streaming stepping or doing nothing // time_to_event > 0 means that we are not in the event if (time_to_event > Seconds(0) and mode == MODE_ALL) { - TimePoint::duration time_since_last_send = now - last_frame_sent; Debug(1, "Time since last send = %.2f s", FPSeconds(time_since_last_send).count()); if (time_since_last_send > Seconds(1)) { char frame_text[64]; @@ -1069,8 +1078,7 @@ void EventStream::runStream() { base_fps, effective_fps); } - now = std::chrono::steady_clock::now(); - TimePoint::duration elapsed = now - start; + TimePoint::duration elapsed = std::chrono::steady_clock::now() - start; delta -= std::chrono::duration_cast(elapsed); // sending frames takes time, so remove it from the sleep time Debug(2, "New delta: %fs from last frame offset %fs - next_frame_offset %fs - elapsed %fs", @@ -1090,6 +1098,11 @@ void EventStream::runStream() { curr_frame_id += step; } // end if !paused } // end scope for mutex lock + + if (type == STREAM_SINGLE) { + Debug(1, "Single, exiting."); + break; + } if (type != STREAM_MPEG) { if (delta > Seconds(0)) { diff --git a/src/zm_ffmpeg.h b/src/zm_ffmpeg.h index 0e3219a144..dfe2140d8d 100644 --- a/src/zm_ffmpeg.h +++ b/src/zm_ffmpeg.h @@ -201,7 +201,7 @@ void zm_dump_codecpar(const AVCodecParameters *par); #ifndef DBG_OFF # define ZM_DUMP_PACKET(pkt, text) \ - Debug(2, "%s: pts: %" PRId64 ", dts: %" PRId64 \ + if (pkt) { Debug(2, "%s: pts: %" PRId64 ", dts: %" PRId64 \ ", size: %d, stream_index: %d, flags: %04x, keyframe(%d) pos: %" PRId64 ", duration: %" AV_PACKET_DURATION_FMT, \ text,\ pkt->pts,\ @@ -211,7 +211,9 @@ void zm_dump_codecpar(const AVCodecParameters *par); pkt->flags,\ pkt->flags & AV_PKT_FLAG_KEY,\ pkt->pos,\ - pkt->duration) + pkt->duration); } else { \ + Error("Null packet send to ZM_DUMP_PACKET"); \ + } # define ZM_DUMP_STREAM_PACKET(stream, pkt, text) \ if (logDebugging()) { \ diff --git a/src/zm_ffmpeg_camera.cpp b/src/zm_ffmpeg_camera.cpp index aba3fe1a83..7ceeb4c7e4 100644 --- a/src/zm_ffmpeg_camera.cpp +++ b/src/zm_ffmpeg_camera.cpp @@ -213,8 +213,9 @@ int FfmpegCamera::Capture(std::shared_ptr &zm_packet) { Info("Unable to read packet from stream %d: error %d \"%s\".", packet->stream_index, ret, av_make_error_string(ret).c_str()); } else { - Error("Unable to read packet from stream %d: error %d \"%s\".", - packet->stream_index, ret, av_make_error_string(ret).c_str()); + logPrintf(Logger::ERROR + monitor->Importance(), + "Unable to read packet from stream %d: error %d \"%s\".", + packet->stream_index, ret, av_make_error_string(ret).c_str()); } return -1; } @@ -235,8 +236,9 @@ int FfmpegCamera::Capture(std::shared_ptr &zm_packet) { Info("Unable to read packet from stream %d: error %d \"%s\".", packet->stream_index, ret, av_make_error_string(ret).c_str()); } else { - Error("Unable to read packet from stream %d: error %d \"%s\".", - packet->stream_index, ret, av_make_error_string(ret).c_str()); + logPrintf(Logger::ERROR + monitor->Importance(), + "Unable to read packet from stream %d: error %d \"%s\".", + packet->stream_index, ret, av_make_error_string(ret).c_str()); } return -1; } @@ -258,11 +260,11 @@ int FfmpegCamera::Capture(std::shared_ptr &zm_packet) { // 32-bit wrap around? Info("Suspected 32bit wraparound in input pts. %" PRId64, packet->pts); return -1; - } else if (packet->pts - lastPTS < -20*stream->time_base.den) { - // -20 is for 20 seconds. Avigilon cameras seem to jump around by about 36 constantly + } else if (packet->pts - lastPTS < -10*stream->time_base.den) { + // -10 is for 10 seconds. Avigilon cameras seem to jump around by about 36 constantly double pts_time = static_cast(av_rescale_q(packet->pts, stream->time_base, AV_TIME_BASE_Q)) / AV_TIME_BASE; double last_pts_time = static_cast(av_rescale_q(lastPTS, stream->time_base, AV_TIME_BASE_Q)) / AV_TIME_BASE; - logPrintf(Logger::WARNING + monitor->Importance(), "Stream pts jumped back in time too far. pts %.2f - last pts %.2f = %.2f > 40seconds", + logPrintf(Logger::WARNING + monitor->Importance(), "Stream pts jumped back in time too far. pts %.2f - last pts %.2f = %.2f > 10seconds", pts_time, last_pts_time, pts_time - last_pts_time); if (error_count > 5) return -1; @@ -540,6 +542,8 @@ int FfmpegCamera::OpenFfmpeg() { if (!mOptions.empty()) { ret = av_dict_parse_string(&opts, mOptions.c_str(), "=", ",", 0); + // reorder_queue is for avformat not codec + av_dict_set(&opts, "reorder_queue_size", nullptr, AV_DICT_MATCH_CASE); } ret = avcodec_open2(mVideoCodecContext, mVideoCodec, &opts); diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 26e69644c0..fca64dd7f8 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -258,7 +258,7 @@ Image::Image(const AVFrame *frame, int p_width, int p_height) : static void dont_free(void *opaque, uint8_t *data) { } -int Image::PopulateFrame(AVFrame *frame) { +int Image::PopulateFrame(AVFrame *frame) const { Debug(1, "PopulateFrame: width %d height %d linesize %d colours %d imagesize %d %s", width, height, linesize, colours, size, av_get_pix_fmt_name(imagePixFormat) diff --git a/src/zm_image.h b/src/zm_image.h index 566e2477b2..92975de4c5 100644 --- a/src/zm_image.h +++ b/src/zm_image.h @@ -207,7 +207,7 @@ class Image { const size_t buffer_size, const int p_buffertype); - int PopulateFrame(AVFrame *frame); + int PopulateFrame(AVFrame *frame) const; inline void CopyBuffer(const Image &image) { Assign(image); diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index d2f287e290..9e102f3be5 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -94,7 +94,7 @@ std::string load_monitor_sql = "`Decoder`, `DecoderHWAccelName`, `DecoderHWAccelDevice`, `RTSPDescribe`, " "`SaveJPEGs`, `VideoWriter`, `EncoderParameters`, " "`OutputCodec`, `Encoder`, `OutputContainer`, " - "`RecordAudio`, " + "`RecordAudio`, WallClockTimestamps," "`Brightness`, `Contrast`, `Hue`, `Colour`, " "`EventPrefix`, `LabelFormat`, `LabelX`, `LabelY`, `LabelSize`," "`ImageBufferCount`, `MaxImageBufferCount`, `WarmupCount`, `PreEventCount`, " @@ -223,6 +223,7 @@ Monitor::Monitor() : output_container(""), imagePixFormat(AV_PIX_FMT_NONE), record_audio(false), + wallclock_timestamps(false), //event_prefix //label_format label_coord(Vector2(0,0)), @@ -265,7 +266,7 @@ Monitor::Monitor() : purpose(QUERY), last_camera_bytes(0), event_count(0), - image_count(0), + //image_count(0), last_capture_image_count(0), analysis_image_count(0), decoding_image_count(0), @@ -355,7 +356,7 @@ Monitor::Monitor() : "Device, Channel, Format, V4LMultiBuffer, V4LCapturesPerFrame, " // V4L Settings "Protocol, Method, Options, User, Pass, Host, Port, Path, SecondPath, Width, Height, Colours, Palette, Orientation+0, Deinterlacing, RTSPDescribe, " "SaveJPEGs, VideoWriter, EncoderParameters, - "OutputCodec, Encoder, OutputContainer, RecordAudio, " + "OutputCodec, Encoder, OutputContainer, RecordAudio, WallClockTimestamps," "Brightness, Contrast, Hue, Colour, " "EventPrefix, LabelFormat, LabelX, LabelY, LabelSize," "ImageBufferCount, `MaxImageBufferCount`, WarmupCount, PreEventCount, PostEventCount, StreamReplayBuffer, AlarmFrameCount, " @@ -549,6 +550,8 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) { col++; record_audio = (*dbrow[col] != '0'); col++; + wallclock_timestamps = (*dbrow[col] != '0'); + col++; /* "Brightness, Contrast, Hue, Colour, " */ brightness = atoi(dbrow[col]); @@ -603,7 +606,7 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) { col++; if (section_length < min_section_length) { section_length = min_section_length; - Warning("Section length %ld < Min Section Length %ld. This is invalid.", + Warning("Section length %jd < Min Section Length %jd. This is invalid.", Seconds(section_length).count(), Seconds(min_section_length).count() ); @@ -725,7 +728,7 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) { // How many frames we need to have before we start analysing ready_count = std::max(warmup_count, pre_event_count); - image_count = 0; + //shared_data->image_count = 0; last_alarm_count = 0; state = IDLE; last_signal = true; // Defaulting to having signal so that we don't get a signal change on the first frame. @@ -963,7 +966,7 @@ bool Monitor::connect() { Warning("Already connected. Please call disconnect first."); } if (!camera) LoadCamera(); - uint64_t image_size = camera->ImageSize(); + size_t image_size = camera->ImageSize(); mem_size = sizeof(SharedData) + sizeof(TriggerData) + (zone_count * sizeof(int)) // Per zone scores @@ -980,9 +983,9 @@ bool Monitor::connect() { "zone_count %d * sizeof int %zu " "VideoStoreData=%zu " "timestamps=%zu " - "images=%dx%" PRIi64 " = %" PRId64 " " - "analysis images=%dx%" PRIi64 " = %" PRId64 " " - "image_format = %dx%" PRIi64 " = %" PRId64 " " + "images=%dx%zu = %zu " + "analysis images=%dx%zu = %zu " + "image_format = %dx%zu = %zu " "total=%jd", sizeof(SharedData), sizeof(TriggerData), @@ -990,9 +993,9 @@ bool Monitor::connect() { sizeof(int), sizeof(VideoStoreData), (image_buffer_count * sizeof(struct timeval)), - image_buffer_count, image_size, (image_buffer_count * image_size), - image_buffer_count, image_size, (image_buffer_count * image_size), - image_buffer_count, sizeof(AVPixelFormat), (image_buffer_count * sizeof(AVPixelFormat)), + image_buffer_count, image_size, static_cast((image_buffer_count * image_size)), + image_buffer_count, image_size, static_cast((image_buffer_count * image_size)), + image_buffer_count, sizeof(AVPixelFormat), static_cast(image_buffer_count * sizeof(AVPixelFormat)), static_cast(mem_size)); #if ZM_MEM_MAPPED mem_file = stringtf("%s/zm.mmap.%u", staticConfig.PATH_MAP.c_str(), id); @@ -1198,9 +1201,11 @@ bool Monitor::connect() { const char *RequestMessageID = soap_wsa_compl ? soap_wsa_rand_uuid(soap) : "RequestMessageID"; if ((!soap_wsa_compl) || (soap_wsa_request(soap, RequestMessageID, proxyEvent.soap_endpoint, "CreatePullPointSubscriptionRequest") == SOAP_OK)) { Debug(1, "ONVIF Endpoint: %s", proxyEvent.soap_endpoint); - if (proxyEvent.CreatePullPointSubscription(&request, response) != SOAP_OK) { + int rc = proxyEvent.CreatePullPointSubscription(&request, response); + + if (rc != SOAP_OK) { const char *detail = soap_fault_detail(soap); - Error("ONVIF Couldn't create subscription! %s, %s", soap_fault_string(soap), detail ? detail : "null"); + Error("ONVIF Couldn't create subscription! %d, fault:%s, detail:%s", rc, soap_fault_string(soap), detail ? detail : "null"); _wsnt__Unsubscribe wsnt__Unsubscribe; _wsnt__UnsubscribeResponse wsnt__UnsubscribeResponse; proxyEvent.Unsubscribe(response.SubscriptionReference.Address, NULL, &wsnt__Unsubscribe, wsnt__UnsubscribeResponse); @@ -1288,6 +1293,7 @@ bool Monitor::connect() { // We set these here because otherwise the first fps calc is meaningless last_fps_time = std::chrono::system_clock::now(); last_analysis_fps_time = std::chrono::system_clock::now(); + last_capture_image_count = 0; Debug(3, "Success connecting"); return true; @@ -1785,7 +1791,7 @@ void Monitor::DumpZoneImage(const char *zone_string) { } // end void Monitor::DumpZoneImage(const char *zone_string) void Monitor::DumpImage(Image *dump_image) const { - if (image_count && !(image_count % 10)) { + if (shared_data->image_count && !(shared_data->image_count % 10)) { std::string filename = stringtf("Monitor%u.jpg", id); std::string new_filename = stringtf("Monitor%u-new.jpg", id); @@ -1858,16 +1864,16 @@ void Monitor::CheckAction() { if (shared_data->action) { // Can there be more than 1 bit set in the action? Shouldn't these be elseifs? if (shared_data->action & RELOAD) { - Info("Received reload indication at count %d", image_count); + Info("Received reload indication at count %d", shared_data->image_count); shared_data->action &= ~RELOAD; Reload(); } if (shared_data->action & SUSPEND) { if (Active()) { - Info("Received suspend indication at count %d", image_count); + Info("Received suspend indication at count %d", shared_data->image_count); shared_data->analysing = ANALYSING_NONE; } else { - Info("Received suspend indication at count %d, but wasn't active", image_count); + Info("Received suspend indication at count %d, but wasn't active", shared_data->image_count); } if (config.max_suspend_time) { SystemTimePoint now = std::chrono::system_clock::now(); @@ -1876,7 +1882,7 @@ void Monitor::CheckAction() { shared_data->action &= ~SUSPEND; } else if (shared_data->action & RESUME) { if ( Enabled() && !Active() ) { - Info("Received resume indication at count %d", image_count); + Info("Received resume indication at count %d", shared_data->image_count); shared_data->analysing = analysing; ref_image.DumpImgBuffer(); // Will get re-assigned by analysis thread shared_data->alarm_x = shared_data->alarm_y = -1; @@ -1888,7 +1894,7 @@ void Monitor::CheckAction() { if (auto_resume_time.time_since_epoch() != Seconds(0)) { SystemTimePoint now = std::chrono::system_clock::now(); if (now >= auto_resume_time) { - Info("Auto resuming at count %d", image_count); + Info("Auto resuming at count %d", shared_data->image_count); auto_resume_time = {}; shared_data->analysing = analysing; ref_image.DumpImgBuffer(); // Will get re-assigned by analysis thread @@ -1897,57 +1903,57 @@ void Monitor::CheckAction() { } void Monitor::UpdateFPS() { - if ( fps_report_interval and - ( - !(image_count%fps_report_interval) + SystemTimePoint now = std::chrono::system_clock::now(); + FPSeconds elapsed = now - last_fps_time; + + // If we are too fast, we get div by zero. This seems to happen in the case of audio packets. + // Also only do the update at most 1/sec + if (elapsed > Seconds(1)) { + // # of images per interval / the amount of time it took + double new_capture_fps = (shared_data->image_count - last_capture_image_count) / elapsed.count(); + uint32 new_camera_bytes = camera->Bytes(); + uint32 new_capture_bandwidth = + static_cast((new_camera_bytes - last_camera_bytes) / elapsed.count()); + double new_analysis_fps = (motion_frame_count - last_motion_frame_count) / elapsed.count(); + + Debug(4, "FPS: capture count %d - last capture count %d = %d now:%lf, last %lf, elapsed %lf = capture: %lf fps analysis: %lf fps", + shared_data->image_count, + last_capture_image_count, + shared_data->image_count - last_capture_image_count, + FPSeconds(now.time_since_epoch()).count(), + FPSeconds(last_fps_time.time_since_epoch()).count(), + elapsed.count(), + new_capture_fps, + new_analysis_fps); + + if ( fps_report_interval and + ( + !(shared_data->image_count%fps_report_interval) or - ( (image_count < fps_report_interval) and !(image_count%10) ) - ) - ) { - SystemTimePoint now = std::chrono::system_clock::now(); - FPSeconds elapsed = now - last_fps_time; - - // If we are too fast, we get div by zero. This seems to happen in the case of audio packets. - // Also only do the update at most 1/sec - if (elapsed > Seconds(1)) { - // # of images per interval / the amount of time it took - double new_capture_fps = (image_count - last_capture_image_count) / elapsed.count(); - uint32 new_camera_bytes = camera->Bytes(); - uint32 new_capture_bandwidth = - static_cast((new_camera_bytes - last_camera_bytes) / elapsed.count()); - double new_analysis_fps = (motion_frame_count - last_motion_frame_count) / elapsed.count(); - - Debug(4, "FPS: capture count %d - last capture count %d = %d now:%lf, last %lf, elapsed %lf = capture: %lf fps analysis: %lf fps", - image_count, - last_capture_image_count, - image_count - last_capture_image_count, - FPSeconds(now.time_since_epoch()).count(), - FPSeconds(last_fps_time.time_since_epoch()).count(), - elapsed.count(), - new_capture_fps, - new_analysis_fps); - + ( (shared_data->image_count < fps_report_interval) and !(shared_data->image_count%10) ) + ) + ) { Info("%s: %d - Capturing at %.2lf fps, capturing bandwidth %ubytes/sec Analysing at %.2lf fps", - name.c_str(), image_count, new_capture_fps, new_capture_bandwidth, new_analysis_fps); + name.c_str(), shared_data->image_count, new_capture_fps, new_capture_bandwidth, new_analysis_fps); #if MOSQUITTOPP_FOUND if (mqtt) mqtt->send(stringtf("Capturing at %.2lf fps, capturing bandwidth %ubytes/sec Analysing at %.2lf fps", - new_capture_fps, new_capture_bandwidth, new_analysis_fps)); + new_capture_fps, new_capture_bandwidth, new_analysis_fps)); #endif - shared_data->capture_fps = new_capture_fps; - last_fps_time = now; - last_capture_image_count = image_count; - shared_data->analysis_fps = new_analysis_fps; - last_motion_frame_count = motion_frame_count; - last_camera_bytes = new_camera_bytes; + } // end if fps_report_interval + shared_data->capture_fps = new_capture_fps; + last_capture_image_count = shared_data->image_count; + shared_data->analysis_fps = new_analysis_fps; + last_motion_frame_count = motion_frame_count; + last_camera_bytes = new_camera_bytes; + last_fps_time = now; std::string sql = stringtf( - "UPDATE LOW_PRIORITY Monitor_Status SET Status='Connected', CaptureFPS = %.2lf, CaptureBandwidth=%u, AnalysisFPS = %.2lf, UpdatedOn=NOW() WHERE MonitorId=%u", - new_capture_fps, new_capture_bandwidth, new_analysis_fps, id); + "UPDATE LOW_PRIORITY Monitor_Status SET Status='Connected', CaptureFPS = %.2lf, CaptureBandwidth=%u, AnalysisFPS = %.2lf, UpdatedOn=NOW() WHERE MonitorId=%u", + new_capture_fps, new_capture_bandwidth, new_analysis_fps, id); dbQueue.push(std::move(sql)); - } // now != last_fps_time - } // end if report fps + } // now != last_fps_time } // void Monitor::UpdateFPS() //Thread where ONVIF polling, and other similar status polling can happen. @@ -2550,17 +2556,17 @@ bool Monitor::Analyse() { Debug(3, "Not video, not clearing packets"); } - if (event) { - event->AddPacket(snap); - } else { + if (!event) { // In the case where people have pre-alarm frames, the web ui will generate the frame images // from the mp4. So no one will notice anyways. - if (snap->image and ((videowriter == PASSTHROUGH) || shared_data->recording == RECORDING_NONE)) { - if (!savejpegs) { - Debug(1, "Deleting image data for %d", snap->image_index); - // Don't need raw images anymore - delete snap->image; - snap->image = nullptr; + if (snap->image) { + if ((videowriter == PASSTHROUGH) || shared_data->recording == RECORDING_NONE) { + if (!savejpegs) { + Debug(1, "Deleting image data for %d", snap->image_index); + // Don't need raw images anymore + delete snap->image; + snap->image = nullptr; + } } if (snap->analysis_image and !(savejpegs & 2)) { Debug(1, "Deleting analysis image data for %d", snap->image_index); @@ -2569,11 +2575,11 @@ bool Monitor::Analyse() { } } // Free up the decoded frame as well, we won't be using it for anything at this time. - snap->out_frame = nullptr; + //snap->out_frame = nullptr; } } // end scope for event_lock - delete packet_lock; + packetqueue.unlock(packet_lock); packetqueue.increment_it(analysis_it); shared_data->last_read_time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); @@ -2589,7 +2595,7 @@ void Monitor::Reload() { { std::lock_guard lck(event_mutex); if (event) { - Info("%s: %03d - Closing event %" PRIu64 ", reloading", name.c_str(), image_count, event->Id()); + Info("%s: %03d - Closing event %" PRIu64 ", reloading", name.c_str(), shared_data->image_count, event->Id()); closeEvent(); } } @@ -2680,7 +2686,7 @@ std::vector> Monitor::LoadMonitors(const std::string &w std::vector> monitors; monitors.reserve(n_monitors); - for (int i = 0; MYSQL_ROW dbrow = mysql_fetch_row(result); i++) { + while(MYSQL_ROW dbrow = mysql_fetch_row(result)) { monitors.emplace_back(std::make_shared()); monitors.back()->Load(dbrow, true, purpose); } @@ -2741,18 +2747,18 @@ std::vector> Monitor::LoadFfmpegMonitors(const char *fi * Returns -1 on failure. */ int Monitor::Capture() { - unsigned int index = image_count % image_buffer_count; + unsigned int index = shared_data->image_count % image_buffer_count; if (image_buffer.empty() or (index >= image_buffer.size())) { Error("Image Buffer is invalid. Check ImageBufferCount. size is %zu", image_buffer.size()); return -1; } std::shared_ptr packet = std::make_shared(); - packet->image_index = image_count; + packet->image_index = shared_data->image_count; packet->timestamp = std::chrono::system_clock::now(); shared_data->heartbeat_time = std::chrono::system_clock::to_time_t(packet->timestamp); int captureResult = camera->Capture(packet); - Debug(4, "Back from capture result=%d image count %d", captureResult, image_count); + Debug(4, "Back from capture result=%d image count %d", captureResult, shared_data->image_count); if (captureResult < 0) { // Unable to capture image @@ -2769,7 +2775,7 @@ int Monitor::Capture() { image_buffer[index]->Assign(*capture_image); shared_timestamps[index] = zm::chrono::duration_cast(packet->timestamp.time_since_epoch()); delete capture_image; - image_count++; + shared_data->image_count++; // What about timestamping it? // Don't want to do analysis on it, but we won't due to signal return -1; @@ -2779,9 +2785,10 @@ int Monitor::Capture() { if (decoding == DECODING_NONE) { shared_data->last_write_index = index; shared_data->last_write_time = std::chrono::system_clock::to_time_t(packet->timestamp); + packet->decoded = true; } Debug(2, "Have packet stream_index:%d ?= videostream_id: %d q.vpktcount %d event? %d image_count %d", - packet->packet->stream_index, video_stream_id, packetqueue.packet_count(video_stream_id), ( event ? 1 : 0 ), image_count); + packet->packet->stream_index, video_stream_id, packetqueue.packet_count(video_stream_id), ( event ? 1 : 0 ), shared_data->image_count); if (packet->codec_type == AVMEDIA_TYPE_VIDEO) { packet->packet->stream_index = video_stream_id; // Convert to packetQueue's index @@ -2816,7 +2823,7 @@ int Monitor::Capture() { return 1; } // end if audio - image_count++; + shared_data->image_count++; // Will only be queued if there are iterators allocated in the queue. packetqueue.queuePacket(packet); @@ -2907,6 +2914,7 @@ bool Monitor::Decode() { if (!packet_lock) return false; std::shared_ptr packet = packet_lock->packet_; if (packet->codec_type != AVMEDIA_TYPE_VIDEO) { + packet->decoded = true; Debug(4, "Not video"); //packetqueue.unlock(packet_lock); delete packet_lock; @@ -3051,8 +3059,8 @@ bool Monitor::Decode() { unsigned int index = shared_data->last_write_index; index++; - decoding_image_count++; index = index % image_buffer_count; + decoding_image_count++; image_buffer[index]->Assign(*(packet->image)); image_pixelformats[index] = packet->image->AVPixFormat(); shared_timestamps[index] = zm::chrono::duration_cast(packet->timestamp.time_since_epoch()); @@ -3138,32 +3146,29 @@ Event * Monitor::openEvent( // FIXME this iterator is not protected from invalidation packetqueue_iterator *start_it = packetqueue.get_event_start_packet_it( - *analysis_it, - (cause == "Continuous" ? 0 : (pre_event_count > alarm_frame_count ? pre_event_count : alarm_frame_count)) - ); - - // This gets a lock on the starting packet + *analysis_it, + (cause == "Continuous" ? 0 : (pre_event_count > alarm_frame_count ? pre_event_count : alarm_frame_count)) + ); - std::shared_ptr starting_packet; - ZMLockedPacket *starting_packet_lock = nullptr; if (*start_it != *analysis_it) { - starting_packet_lock = packetqueue.get_packet(start_it); + ZMLockedPacket *starting_packet_lock = packetqueue.get_packet(start_it); if (!starting_packet_lock) { Warning("Unable to get starting packet lock"); return nullptr; } - starting_packet = starting_packet_lock->packet_; + std::shared_ptr starting_packet = starting_packet_lock->packet_; + delete starting_packet_lock; ZM_DUMP_PACKET(starting_packet->packet, "First packet from start"); + event = new Event(this, start_it, starting_packet->timestamp, cause, noteSetMap); + SetVideoWriterStartTime(starting_packet->timestamp); } else { - starting_packet = snap; - ZM_DUMP_PACKET(starting_packet->packet, "First packet from alarm"); + ZM_DUMP_PACKET(snap->packet, "First packet from alarm"); + event = new Event(this, start_it, snap->timestamp, cause, noteSetMap); + SetVideoWriterStartTime(snap->timestamp); } - event = new Event(this, starting_packet->timestamp, cause, noteSetMap); - shared_data->last_event_id = event->Id(); - SetVideoWriterStartTime(starting_packet->timestamp); strncpy(shared_data->alarm_cause, cause.c_str(), sizeof(shared_data->alarm_cause)-1); #if MOSQUITTOPP_FOUND @@ -3184,23 +3189,6 @@ Event * Monitor::openEvent( } } - // Write out starting packets, do not modify packetqueue it will garbage collect itself - while (starting_packet_lock && (*start_it != *analysis_it) && !zm_terminate) { - ZM_DUMP_PACKET(starting_packet_lock->packet_->packet, "Queuing packet for event"); - - event->AddPacket(starting_packet_lock->packet_); - delete starting_packet_lock; - starting_packet_lock = nullptr; - - packetqueue.increment_it(start_it); - if ((*start_it) != *analysis_it) { - starting_packet_lock = packetqueue.get_packet(start_it); - } - } - packetqueue.free_it(start_it); - delete start_it; - start_it = nullptr; - return event; } @@ -3570,7 +3558,7 @@ int Monitor::Pause() { { std::lock_guard lck(event_mutex); if (event) { - Info("%s: image_count:%d - Closing event %" PRIu64 ", shutting down", name.c_str(), image_count, event->Id()); + Info("%s: image_count:%d - Closing event %" PRIu64 ", shutting down", name.c_str(), shared_data->image_count, event->Id()); closeEvent(); close_event_thread.join(); } diff --git a/src/zm_monitor.h b/src/zm_monitor.h index 4fb98b7274..b49c5b933c 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -179,59 +179,60 @@ class Monitor : public std::enable_shared_from_this { uint32_t size; /* +0 */ int32_t last_write_index; /* +4 */ int32_t last_read_index; /* +8 */ - uint32_t state; /* +12 */ + int32_t image_count; /* +12 */ + uint32_t state; /* +16 */ double capture_fps; // Current capturing fps double analysis_fps; // Current analysis fps double latitude; double longitude; - uint64_t last_event_id; /* +16 */ - uint32_t action; /* +24 */ - int32_t brightness; /* +28 */ - int32_t hue; /* +32 */ - int32_t colour; /* +36 */ - int32_t contrast; /* +40 */ - int32_t alarm_x; /* +44 */ - int32_t alarm_y; /* +48 */ - uint8_t valid; /* +52 */ - uint8_t capturing; /* +53 */ - uint8_t analysing; /* +54 */ - uint8_t recording; /* +55 */ - uint8_t signal; /* +56 */ - uint8_t format; /* +57 */ - uint8_t reserved1; /* +58 */ - uint8_t reserved2; /* +59 */ - uint32_t imagesize; /* +60 */ - uint32_t last_frame_score; /* +64 */ - uint32_t audio_frequency; /* +68 */ - uint32_t audio_channels; /* +72 */ - uint32_t reserved3; /* +76 */ + uint64_t last_event_id; /* +48 */ + uint32_t action; /* +56 */ + int32_t brightness; /* +60 */ + int32_t hue; /* +64 */ + int32_t colour; /* +68 */ + int32_t contrast; /* +72 */ + int32_t alarm_x; /* +76 */ + int32_t alarm_y; /* +80 */ + uint8_t valid; /* +81 */ + uint8_t capturing; /* +82 */ + uint8_t analysing; /* +83 */ + uint8_t recording; /* +84 */ + uint8_t signal; /* +85 */ + uint8_t format; /* +86 */ + uint8_t reserved1; /* +87 */ + //uint8_t reserved2; /* +0 */ + uint32_t imagesize; /* +88 */ + uint32_t last_frame_score; /* +72 */ + uint32_t audio_frequency; /* +76 */ + uint32_t audio_channels; /* +80 */ + //uint32_t reserved3; /* +0 */ /* ** This keeps 32bit time_t and 64bit time_t identical and compatible as long as time is before 2038. ** Shared memory layout should be identical for both 32bit and 64bit and is multiples of 16. ** Because startup_time is 64bit it may be aligned to a 64bit boundary. So it's offset SHOULD be a multiple ** of 8. Add or delete epadding's to achieve this. */ - union { /* +80 */ + union { /* +84 */ time_t startup_time; /* When the zmc process started. zmwatch uses this to see how long the process has been running without getting any images */ uint64_t extrapad1; }; - union { /* +88 */ + union { /* +92 */ time_t heartbeat_time; /* Constantly updated by zmc. Used to determine if the process is alive or hung or dead */ uint64_t extrapad2; }; - union { /* +96 */ + union { /* +100 */ time_t last_write_time; uint64_t extrapad3; }; - union { /* +104 */ + union { /* +108 */ time_t last_read_time; uint64_t extrapad4; }; - union { /* +112 */ + union { /* +116 */ time_t last_viewed_time; uint64_t extrapad5; }; - uint8_t control_state[256]; /* +120 */ + uint8_t control_state[256]; /* +124 */ char alarm_cause[256]; char video_fifo_path[64]; @@ -463,6 +464,7 @@ class Monitor : public std::enable_shared_from_this { std::string output_container; _AVPIXELFORMAT imagePixFormat; bool record_audio; // Whether to store the audio that we receive + bool wallclock_timestamps; // Whether to use wallclock pts/dts instead of values from ffmpeg int output_source_stream; @@ -518,7 +520,6 @@ class Monitor : public std::enable_shared_from_this { unsigned int last_camera_bytes; int event_count; - int image_count; int last_capture_image_count; // last value of image_count when calculating capture fps int analysis_image_count; // How many frames have been processed by analysis thread. int decoding_image_count; // How many frames have been processed by analysis thread. @@ -758,6 +759,7 @@ class Monitor : public std::enable_shared_from_this { inline double Longitude() const { return shared_data ? shared_data->longitude : longitude; } inline bool RTSPServer() const { return rtsp_server; } inline bool RecordAudio() const { return record_audio; } + inline bool WallClockTimestamps() const { return wallclock_timestamps; } /* inline Purpose Purpose() { return purpose }; diff --git a/src/zm_monitor_rtsp2web.cpp b/src/zm_monitor_rtsp2web.cpp index 9dca0ec59a..db62a36952 100644 --- a/src/zm_monitor_rtsp2web.cpp +++ b/src/zm_monitor_rtsp2web.cpp @@ -81,7 +81,8 @@ int Monitor::RTSP2WebManager::check_RTSP2Web() { return -1; } - Debug(1, "Queried for stream status: %s", remove_newlines(response).c_str()); + response = remove_newlines(response); + Debug(1, "Queried for stream status: %s", response.c_str()); if (response.find("\"status\": 0") != std::string::npos) { if (response.find("stream not found") != std::string::npos) { Debug(1, "Mountpoint Missing"); @@ -132,10 +133,11 @@ int Monitor::RTSP2WebManager::add_to_RTSP2Web() { return -1; } - Debug(1, "Adding stream response: %s", remove_newlines(response).c_str()); + response = remove_newlines(response); + Debug(1, "Adding stream response: %s", response.c_str()); //scan for missing session or handle id "No such session" "no such handle" if (response.find("\"status\": 1") == std::string::npos) { - if (response == "{ \"status\": 0, \"payload\": \"stream already exists\"}") { + if (response == "{ \"status\": 0, \"payload\": \"stream already exists\"}") { Debug(1, "RTSP2Web failed adding stream, response: %s", response.c_str()); } else { Warning("RTSP2Web failed adding stream, response: %s", response.c_str()); diff --git a/src/zm_monitorstream.cpp b/src/zm_monitorstream.cpp index b49df32430..c114ca72e4 100644 --- a/src/zm_monitorstream.cpp +++ b/src/zm_monitorstream.cpp @@ -488,6 +488,7 @@ void MonitorStream::runStream() { // point to end which is theoretically not a valid value because all indexes are % image_buffer_count int32_t last_read_index = monitor->image_buffer_count; + int32_t last_image_count = 0; TimePoint stream_start_time = std::chrono::steady_clock::now(); when_to_send_next_frame = stream_start_time; // initialize it to now so that we spit out a frame immediately @@ -684,15 +685,17 @@ void MonitorStream::runStream() { } } // end if (buffered_playback && delayed) - if (last_read_index != monitor->shared_data->last_write_index) { + if (last_read_index != monitor->shared_data->last_write_index || last_image_count < monitor->shared_data->image_count) { // have a new image to send - int index = monitor->shared_data->last_write_index % monitor->image_buffer_count; + int last_write_index = monitor->shared_data->last_write_index; + int index = last_write_index % monitor->image_buffer_count; //if ((frame_mod == 1) || ((frame_count%frame_mod) == 0)) { if ( now >= when_to_send_next_frame ) { if (!paused && !delayed) { - last_read_index = monitor->shared_data->last_write_index; - Debug(2, "Sending frame index: %d: frame_mod: %d frame count: %d paused(%d) delayed(%d)", - index, frame_mod, frame_count, paused, delayed); + Debug(2, "Sending frame index: %d(%d%%%d): frame_mod: %d frame count: %d last image count %d image count %d paused %d delayed %d", + index, last_write_index, monitor->image_buffer_count, frame_mod, frame_count, last_image_count, monitor->shared_data->image_count, paused, delayed); + last_read_index = last_write_index; + last_image_count = monitor->shared_data->image_count; // Send the next frame // // Perhaps we should use NOW instead. @@ -764,6 +767,8 @@ void MonitorStream::runStream() { } // end if paused or not //} else { //frame_count++; + } else { + Debug(2, "Not time to send next frame."); } // end if should send frame now > when_to_send_next_frame if (buffered_playback && !paused) { @@ -819,17 +824,20 @@ void MonitorStream::runStream() { Debug(3, "Using %f for maxfps. capture_fps: %f maxfps %f * replay_rate: %d = %f", fps, capture_fps, maxfps, replay_rate, sleep_time_seconds); sleep_time = FPSeconds(sleep_time_seconds); - if (when_to_send_next_frame > now) - sleep_time -= when_to_send_next_frame - now; + if (when_to_send_next_frame > now) { + sleep_time -= (when_to_send_next_frame - now); + Debug(2, "Adjusting sleep time for when_to_send_next_frame - now = %f", FPSeconds(when_to_send_next_frame - now).count()); + } - when_to_send_next_frame = now + std::chrono::duration_cast(sleep_time); if (last_frame_sent > now) { FPSeconds elapsed = last_frame_sent - now; if (sleep_time > elapsed) { + Debug(2, "Adjusting sleep time by %f elapsed", elapsed.count()); sleep_time -= elapsed; } } + when_to_send_next_frame = now + std::chrono::duration_cast(sleep_time); } else { sleep_time = when_to_send_next_frame - now; } diff --git a/src/zm_packetqueue.cpp b/src/zm_packetqueue.cpp index ccf707571c..5d3a39d914 100644 --- a/src/zm_packetqueue.cpp +++ b/src/zm_packetqueue.cpp @@ -96,7 +96,7 @@ bool PacketQueue::queuePacket(std::shared_ptr add_packet) { std::shared_ptr prev_packet = *rit; if (prev_packet->packet->stream_index == add_packet->packet->stream_index) { - if (prev_packet->packet->dts >= add_packet->packet->dts) { + if (prev_packet->packet->dts > add_packet->packet->dts) { Debug(1, "Have out of order packets"); ZM_DUMP_PACKET(prev_packet->packet, "queued_packet"); ZM_DUMP_PACKET(add_packet->packet, "add_packet"); @@ -252,24 +252,9 @@ void PacketQueue::clearPackets(const std::shared_ptr &add_packet) { // If analysis_it isn't at the end, we need to keep that many additional packets int tail_count = 0; - if (pktQueue.back() != add_packet) { - packetqueue_iterator it = pktQueue.end(); - --it; - while (*it != add_packet) { - if (!(*it)) { - Error("null packet"); - break; - } - if (!((*it)->packet)) { - Error("null av packet"); - ++tail_count; - } else { - if ((*it)->packet->stream_index == video_stream_id) - ++tail_count; - } - - --it; - } + for (auto it = pktQueue.rbegin(); it != pktQueue.rend() && (*it != add_packet); ++it) { + if ((*it)->packet->stream_index == video_stream_id) + ++tail_count; } Debug(1, "Tail count is %d, queue size is %zu", tail_count, pktQueue.size()); @@ -311,6 +296,7 @@ void PacketQueue::clearPackets(const std::shared_ptr &add_packet) { } int keyframe_interval_count = 1; + int video_packets_to_delete = 0; // This is a count of how many packets we will delete so we know when to stop looking ZMLockedPacket *lp = new ZMLockedPacket(zm_packet); if (!lp->trylock()) { @@ -318,8 +304,14 @@ void PacketQueue::clearPackets(const std::shared_ptr &add_packet) { delete lp; return; } // end if first packet not locked + + if (is_there_an_iterator_pointing_to_packet(zm_packet)) { + Debug(3, "Found iterator Counted %d video packets. Which would leave %d in packetqueue tail count is %d", + video_packets_to_delete, packet_counts[video_stream_id]-video_packets_to_delete, tail_count); + delete lp; + return; + } - int video_packets_to_delete = 0; // This is a count of how many packets we will delete so we know when to stop looking ++it; delete lp; @@ -334,15 +326,11 @@ void PacketQueue::clearPackets(const std::shared_ptr &add_packet) { } delete lp; -#if 0 - // There are no threads that follow analysis thread. So there cannot be an it pointing here - // event writing thread technically follows, but packets are copied out of queue if (is_there_an_iterator_pointing_to_packet(zm_packet)) { - if (pktQueue.begin() == next_front) - Warning("Found iterator at beginning of queue. Some thread isn't keeping up"); + Debug(3, "Found iterator Counted %d video packets. Which would leave %d in packetqueue tail count is %d", + video_packets_to_delete, packet_counts[video_stream_id]-video_packets_to_delete, tail_count); break; } -#endif if (zm_packet->packet->stream_index == video_stream_id) { keyframe_interval_count++; @@ -457,6 +445,32 @@ int PacketQueue::packet_count(int stream_id) { return packet_counts[stream_id]; } // end int PacketQueue::packet_count(int stream_id) +ZMLockedPacket *PacketQueue::get_packet_no_wait(packetqueue_iterator *it) { + if (deleting or zm_terminate) + return nullptr; + + Debug(4, "Locking in get_packet using it %p queue end? %d", + std::addressof(*it), (*it == pktQueue.end())); + + // scope for lock + std::unique_lock lck(mutex); + Debug(4, "Have Lock in get_packet"); + if ((*it == pktQueue.end()) and !(deleting or zm_terminate)) { + Debug(2, "waiting. Queue size %zu it == end? %d", pktQueue.size(), (*it == pktQueue.end())); + condition.wait(lck); + } + if ((*it == pktQueue.end()) or deleting or zm_terminate) return nullptr; + + std::shared_ptr p = *(*it); + ZMLockedPacket *lp = new ZMLockedPacket(p); + if (lp->trylock()) { + Debug(2, "Locked packet %d, unlocking packetqueue mutex", p->image_index); + return lp; + } + delete lp; + return nullptr; +} + // Returns a packet. Packet will be locked ZMLockedPacket *PacketQueue::get_packet(packetqueue_iterator *it) { if (deleting or zm_terminate) @@ -599,6 +613,7 @@ packetqueue_iterator *PacketQueue::get_event_start_packet_it( packetqueue_iterator *it = new packetqueue_iterator; iterators.push_back(it); + Debug(4, "Have event start iterator %p", std::addressof(*it)); *it = snapshot_it; std::shared_ptr packet = *(*it); @@ -698,6 +713,7 @@ packetqueue_iterator * PacketQueue::get_video_it(bool wait) { } // get video_it void PacketQueue::free_it(packetqueue_iterator *it) { + std::unique_lock lck(mutex); for ( std::list::iterator iterators_it = iterators.begin(); iterators_it != iterators.end(); @@ -710,7 +726,7 @@ void PacketQueue::free_it(packetqueue_iterator *it) { } } -bool PacketQueue::is_there_an_iterator_pointing_to_packet(const std::shared_ptr &zm_packet) { +bool PacketQueue::is_there_an_iterator_pointing_to_packet(const std::shared_ptr zm_packet) { for ( std::list::iterator iterators_it = iterators.begin(); iterators_it != iterators.end(); @@ -718,6 +734,7 @@ bool PacketQueue::is_there_an_iterator_pointing_to_packet(const std::shared_ptr< ) { packetqueue_iterator *iterator_it = *iterators_it; if (*iterator_it == pktQueue.end()) { + Debug(4, "Checking iterator %p == end", std::addressof(*iterator_it)); continue; } Debug(4, "Checking iterator %p == packet ? %d", std::addressof(*iterator_it), ( *(*iterator_it) == zm_packet )); diff --git a/src/zm_packetqueue.h b/src/zm_packetqueue.h index fb75ae46ea..c12a467742 100644 --- a/src/zm_packetqueue.h +++ b/src/zm_packetqueue.h @@ -77,6 +77,7 @@ class PacketQueue { bool increment_it(packetqueue_iterator *it); bool increment_it(packetqueue_iterator *it, int stream_id); ZMLockedPacket *get_packet(packetqueue_iterator *); + ZMLockedPacket *get_packet_no_wait(packetqueue_iterator *); ZMLockedPacket *get_packet_and_increment_it(packetqueue_iterator *); packetqueue_iterator *get_video_it(bool wait); packetqueue_iterator *get_stream_it(int stream_id); @@ -86,7 +87,7 @@ class PacketQueue { packetqueue_iterator snapshot_it, unsigned int pre_event_count ); - bool is_there_an_iterator_pointing_to_packet(const std::shared_ptr &zm_packet); + bool is_there_an_iterator_pointing_to_packet(const std::shared_ptr zm_packet); void unlock(ZMLockedPacket *lp); void notify_all(); void wait(); diff --git a/src/zm_signal.cpp b/src/zm_signal.cpp index efeb38c22c..c7ea5d85a6 100644 --- a/src/zm_signal.cpp +++ b/src/zm_signal.cpp @@ -27,6 +27,7 @@ bool zm_reload = false; bool zm_terminate = false; +bool zm_panic = false; RETSIGTYPE zm_hup_handler(int signal) { // Shouldn't do complex things in signal handlers, logging is complex and can block due to mutexes. @@ -47,6 +48,9 @@ RETSIGTYPE zm_die_handler(int signal) #endif { zm_terminate = true; + if (zm_panic) + Fatal("Got signal %d (%s), crashing", signal, strsignal(signal)); + zm_panic = true; Error("Got signal %d (%s), crashing", signal, strsignal(signal)); #if (defined(__i386__) || defined(__x86_64__)) // Get more information if available diff --git a/src/zm_videostore.cpp b/src/zm_videostore.cpp index 554543d351..2d561a59ab 100644 --- a/src/zm_videostore.cpp +++ b/src/zm_videostore.cpp @@ -50,6 +50,7 @@ VideoStore::CodecData VideoStore::codec_data[] = { { AV_CODEC_ID_H264, "h264", "h264_qsv", AV_PIX_FMT_YUV420P, AV_PIX_FMT_QSV, AV_HWDEVICE_TYPE_QSV }, { AV_CODEC_ID_H264, "h264", "h264_nvenc", AV_PIX_FMT_NV12, AV_PIX_FMT_NV12, AV_HWDEVICE_TYPE_NONE }, { AV_CODEC_ID_H264, "h264", "h264_omx", AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P, AV_HWDEVICE_TYPE_NONE }, + { AV_CODEC_ID_H264, "h264", "h264_v4l2m2m", AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P, AV_HWDEVICE_TYPE_NONE }, { AV_CODEC_ID_H264, "h264", "h264", AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P, AV_HWDEVICE_TYPE_NONE }, { AV_CODEC_ID_H264, "h264", "libx264", AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P, AV_HWDEVICE_TYPE_NONE }, { AV_CODEC_ID_MJPEG, "mjpeg", "mjpeg", AV_PIX_FMT_YUVJ422P, AV_PIX_FMT_YUVJ422P, AV_HWDEVICE_TYPE_NONE }, @@ -150,7 +151,11 @@ bool VideoStore::open() { reorder_queue_size = std::stoul(entry->value); // remove it to prevent complaining later. av_dict_set(&opts, "reorder_queue_size", nullptr, AV_DICT_MATCH_CASE); - } else if (monitor->has_out_of_order_packets()) { + } else if (monitor->has_out_of_order_packets() + and !monitor->WallClockTimestamps() + // Only sort packets for passthrough. Encoding uses wallclock by default + and monitor->GetOptVideoWriter() == Monitor::PASSTHROUGH + ) { reorder_queue_size = 2*monitor->get_max_keyframe_interval(); } Debug(1, "reorder_queue_size set to %zu", reorder_queue_size); @@ -300,12 +305,37 @@ bool VideoStore::open() { video_out_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; } + // We have to re-parse the options because each attempt to open destroys the dictionary + AVDictionary *opts = 0; + ret = av_dict_parse_string(&opts, options.c_str(), "=", ",#\n", 0); + if (ret < 0) { + Warning("Could not parse ffmpeg encoder options list '%s'", options.c_str()); + } else { + const AVDictionaryEntry *entry = av_dict_get(opts, "reorder_queue_size", nullptr, AV_DICT_MATCH_CASE); + if (entry) { + reorder_queue_size = std::stoul(entry->value); + Debug(1, "reorder_queue_size set to %zu", reorder_queue_size); + // remove it to prevent complaining later. + av_dict_set(&opts, "reorder_queue_size", nullptr, AV_DICT_MATCH_CASE); + } + } // When encoding, we are going to use the timestamp values instead of packet pts/dts video_out_ctx->time_base = AV_TIME_BASE_Q; video_out_ctx->codec_id = codec_data[i].codec_id; video_out_ctx->pix_fmt = codec_data[i].hw_pix_fmt; Debug(1, "Setting pix fmt to %d %s", codec_data[i].hw_pix_fmt, av_get_pix_fmt_name(codec_data[i].hw_pix_fmt)); - video_out_ctx->level = 32; + const AVDictionaryEntry *opts_level = av_dict_get(opts, "level", nullptr, AV_DICT_MATCH_CASE); + if (opts_level) { + video_out_ctx->level = std::stoul(opts_level->value); + } else { + video_out_ctx->level = 32; + } + const AVDictionaryEntry *opts_gop_size = av_dict_get(opts, "gop_size", nullptr, AV_DICT_MATCH_CASE); + if (opts_gop_size) { + video_out_ctx->gop_size = std::stoul(opts_gop_size->value); + } else { + video_out_ctx->gop_size = 12; + } // Don't have an input stream, so need to tell it what we are sending it, or are transcoding video_out_ctx->width = monitor->Width(); @@ -314,7 +344,6 @@ bool VideoStore::open() { if (video_out_ctx->codec_id == AV_CODEC_ID_H264) { video_out_ctx->bit_rate = 2000000; - video_out_ctx->gop_size = 12; video_out_ctx->max_b_frames = 1; } else if (video_out_ctx->codec_id == AV_CODEC_ID_MPEG2VIDEO) { /* just for testing, we also add B frames */ @@ -363,20 +392,6 @@ bool VideoStore::open() { } // end if hwdevice_type != NONE #endif - // We have to re-parse the options because each attempt to open destroys the dictionary - AVDictionary *opts = 0; - ret = av_dict_parse_string(&opts, options.c_str(), "=", ",#\n", 0); - if (ret < 0) { - Warning("Could not parse ffmpeg encoder options list '%s'", options.c_str()); - } else { - const AVDictionaryEntry *entry = av_dict_get(opts, "reorder_queue_size", nullptr, AV_DICT_MATCH_CASE); - if (entry) { - reorder_queue_size = std::stoul(entry->value); - Debug(1, "reorder_queue_size set to %zu", reorder_queue_size); - // remove it to prevent complaining later. - av_dict_set(&opts, "reorder_queue_size", nullptr, AV_DICT_MATCH_CASE); - } - } if ((ret = avcodec_open2(video_out_ctx, video_out_codec, &opts)) < 0) { if (wanted_encoder != "" and wanted_encoder != "auto") { Warning("Can't open video codec (%s) %s", @@ -1011,7 +1026,7 @@ bool VideoStore::setup_resampler() { return true; } // end bool VideoStore::setup_resampler() -int VideoStore::writePacket(const std::shared_ptr &zm_pkt) { +int VideoStore::writePacket(const std::shared_ptr zm_pkt) { int stream_index; if (zm_pkt->codec_type == AVMEDIA_TYPE_VIDEO) { stream_index = video_out_stream->index; @@ -1071,7 +1086,7 @@ int VideoStore::writePacket(const std::shared_ptr &zm_pkt) { return 0; } -int VideoStore::writeVideoFramePacket(const std::shared_ptr &zm_packet) { +int VideoStore::writeVideoFramePacket(const std::shared_ptr zm_packet) { av_packet_guard pkt_guard; #if HAVE_LIBAVUTIL_HWCONTEXT_H av_frame_ptr hw_frame; @@ -1272,21 +1287,33 @@ int VideoStore::writeVideoFramePacket(const std::shared_ptr &zm_packet av_packet_ref(opkt.get(), ipkt); pkt_guard.acquire(opkt); - if (ipkt->dts != AV_NOPTS_VALUE) { + if (monitor->WallClockTimestamps()) { + int64_t ts = static_cast(std::chrono::duration_cast(zm_packet->timestamp.time_since_epoch()).count()); if (video_first_dts == AV_NOPTS_VALUE) { - Debug(2, "Starting video first_dts will become %" PRId64, ipkt->dts); - video_first_dts = ipkt->dts; + Debug(2, "Starting video first_dts will become %" PRId64, ts); + video_first_dts = ts; } - opkt->dts = ipkt->dts - video_first_dts; - //} else { - //opkt.dts = next_dts[video_out_stream->index] ? av_rescale_q(next_dts[video_out_stream->index], video_out_stream->time_base, video_in_stream->time_base) : 0; - //Debug(3, "Setting dts to video_next_dts %" PRId64 " from %" PRId64, opkt.dts, next_dts[video_out_stream->index]); - } - if ((ipkt->pts != AV_NOPTS_VALUE) and (video_first_dts != AV_NOPTS_VALUE)) { - opkt->pts = ipkt->pts - video_first_dts; - } + opkt->pts = opkt->dts = av_rescale_q(ts-video_first_dts, AV_TIME_BASE_Q, video_out_stream->time_base); - av_packet_rescale_ts(opkt.get(), video_in_stream->time_base, video_out_stream->time_base); + Debug(2, "dts from timestamp, set to (%" PRId64 ") secs(%.2f), minus first_dts %" PRId64 " = %" PRId64, + ts, + FPSeconds(zm_packet->timestamp.time_since_epoch()).count(), + video_first_dts, ts - video_first_dts); + } else { + if (ipkt->dts != AV_NOPTS_VALUE) { + if (video_first_dts == AV_NOPTS_VALUE) { + Debug(2, "Starting video first_dts will become %" PRId64, ipkt->dts); + video_first_dts = ipkt->dts; + } + opkt->dts = ipkt->dts - video_first_dts; + } + if (ipkt->pts != AV_NOPTS_VALUE) { + opkt->pts = ipkt->pts - video_first_dts; + } + if ((ipkt->pts != AV_NOPTS_VALUE) and (ipkt->dts != AV_NOPTS_VALUE)) { + av_packet_rescale_ts(opkt.get(), video_in_stream->time_base, video_out_stream->time_base); + } + } // end if wallclock or not } // end if codec matches write_packet(opkt.get(), video_out_stream); @@ -1294,10 +1321,18 @@ int VideoStore::writeVideoFramePacket(const std::shared_ptr &zm_packet return 1; } // end int VideoStore::writeVideoFramePacket( AVPacket *ipkt ) -int VideoStore::writeAudioFramePacket(const std::shared_ptr &zm_packet) { +int VideoStore::writeAudioFramePacket(const std::shared_ptr zm_packet) { AVPacket *ipkt = zm_packet->packet.get(); ZM_DUMP_STREAM_PACKET(audio_in_stream, ipkt, "input packet"); + if (monitor->WallClockTimestamps()) { + int64_t ts = static_cast(std::chrono::duration_cast(zm_packet->timestamp.time_since_epoch()).count()); + ipkt->pts = ipkt->dts = av_rescale_q(ts, AV_TIME_BASE_Q, audio_in_stream->time_base); + + Debug(2, "dts from timestamp, set to (%" PRId64 ") secs(%.2f)", + ts, FPSeconds(zm_packet->timestamp.time_since_epoch()).count()); + } + if (audio_first_dts == AV_NOPTS_VALUE) { audio_first_dts = ipkt->dts; audio_next_pts = audio_out_ctx->frame_size; @@ -1380,18 +1415,28 @@ int VideoStore::write_packet(AVPacket *pkt, AVStream *stream) { if (pkt->dts == AV_NOPTS_VALUE) { Debug(1, "undef dts, fixing by setting to stream last_dts %" PRId64, last_dts[stream->index]); if (last_dts[stream->index] == AV_NOPTS_VALUE) { - last_dts[stream->index] = 0; - } + last_dts[stream->index] = -1; + } pkt->dts = last_dts[stream->index]; } else { - if ((last_dts[stream->index] != AV_NOPTS_VALUE) and (pkt->dts <= last_dts[stream->index])) { - Warning("non increasing dts, fixing. our dts %" PRId64 " stream %d last_dts %" PRId64 ". reorder_queue_size=%zu", - pkt->dts, stream->index, last_dts[stream->index], reorder_queue_size); - // dts MUST monotonically increase, so add 1 which should be a small enough time difference to not matter. - pkt->dts = last_dts[stream->index]+1; + if (last_dts[stream->index] != AV_NOPTS_VALUE) { + if (pkt->dts < last_dts[stream->index]) { + Warning("non increasing dts, fixing. our dts %" PRId64 " stream %d last_dts %" PRId64 " last_duration %" PRId64 ". reorder_queue_size=%zu", + pkt->dts, stream->index, last_dts[stream->index], last_duration[stream->index], reorder_queue_size); + pkt->dts = last_dts[stream->index]+last_duration[stream->index]; + if (pkt->dts > pkt->pts) pkt->pts = pkt->dts; // Do it here to avoid warning below + } else if (pkt->dts == last_dts[stream->index]) { + // Commonly seen + Debug(1, "non increasing dts, fixing. our dts %" PRId64 " stream %d last_dts %" PRId64 " stream %d. reorder_queue_size=%zu", + pkt->dts, stream->index, last_dts[stream->index], stream->index, reorder_queue_size); + // dts MUST monotonically increase, so add 1 which should be a small enough time difference to not matter. + pkt->dts = last_dts[stream->index]+last_duration[stream->index]; + if (pkt->dts > pkt->pts) pkt->pts = pkt->dts; // Do it here to avoid warning below + } } next_dts[stream->index] = pkt->dts + pkt->duration; last_dts[stream->index] = pkt->dts; + last_duration[stream->index] = pkt->duration; } if (pkt->pts == AV_NOPTS_VALUE) { diff --git a/src/zm_videostore.h b/src/zm_videostore.h index fc18c47ce8..8f1b94a83d 100644 --- a/src/zm_videostore.h +++ b/src/zm_videostore.h @@ -86,6 +86,7 @@ class VideoStore { // These are for out, should start at zero. We assume they do not wrap because we just aren't going to save files that big. int64_t *next_dts; std::map last_dts; + std::map last_duration; int64_t audio_next_pts; int max_stream_index; @@ -108,11 +109,11 @@ class VideoStore { ~VideoStore(); bool open(); - void write_video_packet(AVPacket &pkt); - void write_audio_packet(AVPacket &pkt); - int writeVideoFramePacket(const std::shared_ptr &pkt); - int writeAudioFramePacket(const std::shared_ptr &pkt); - int writePacket(const std::shared_ptr &pkt); + void write_video_packet(AVPacket pkt); + void write_audio_packet(AVPacket pkt); + int writeVideoFramePacket(const std::shared_ptr pkt); + int writeAudioFramePacket(const std::shared_ptr pkt); + int writePacket(const std::shared_ptr pkt); int write_packets(PacketQueue &queue); void flush_codecs(); const char *get_codec() { diff --git a/src/zms.cpp b/src/zms.cpp index 09d4cd08c1..57b7798b9e 100644 --- a/src/zms.cpp +++ b/src/zms.cpp @@ -24,6 +24,8 @@ #include "zm_monitorstream.h" #include "zm_eventstream.h" #include "zm_fifo_stream.h" +#include +#include #include #include @@ -154,6 +156,12 @@ int main(int argc, const char *argv[], char **envp) { monitor_id = atoi(value); if ( source == ZMS_UNKNOWN ) source = ZMS_MONITOR; + } else if ( !strcmp(name, "datetime") ) { + std::tm tm = {}; + std::stringstream ss(value); + ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S"); + auto tp = std::chrono::system_clock::from_time_t(std::mktime(&tm)); + event_time = std::chrono::duration_cast(tp.time_since_epoch()).count(); } else if ( !strcmp(name, "time") ) { event_time = atoi(value); } else if ( !strcmp(name, "event") ) { @@ -166,6 +174,10 @@ int main(int argc, const char *argv[], char **envp) { frames_to_send = strtoll(value, nullptr, 10); } else if ( !strcmp(name, "scale") ) { scale = atoi(value); + if (scale > 1600) { + Warning("Limiting scale to 16x"); + scale = 1600; + } } else if ( !strcmp(name, "rate") ) { rate = atoi(value); } else if ( !strcmp(name, "maxfps") ) { @@ -320,6 +332,7 @@ int main(int argc, const char *argv[], char **envp) { stream.setStreamMaxFPS(maxfps); stream.setStreamMode(replay); stream.setStreamQueue(connkey); + stream.setFramesToSend(frames_to_send); if ( monitor_id && event_time ) { stream.setStreamStart(monitor_id, event_time); } else { @@ -329,6 +342,8 @@ int main(int argc, const char *argv[], char **envp) { stream.setStreamFrameType(analysis_frames ? StreamBase::FRAME_ANALYSIS: StreamBase::FRAME_NORMAL); if ( mode == ZMS_JPEG ) { stream.setStreamType(EventStream::STREAM_JPEG); + } else if ( mode == ZMS_SINGLE ) { + stream.setStreamType(MonitorStream::STREAM_SINGLE); } else { stream.setStreamFormat(format); stream.setStreamBitrate(bitrate); diff --git a/utils/packpack/startpackpack.sh b/utils/packpack/startpackpack.sh index 97a06675ab..2486f746ab 100755 --- a/utils/packpack/startpackpack.sh +++ b/utils/packpack/startpackpack.sh @@ -381,7 +381,7 @@ elif [ "${OS}" == "debian" ] || [ "${OS}" == "ubuntu" ] || [ "${OS}" == "raspbia setdebpkgname movecrud - if [ "${DIST}" == "bionic" ] || [ "${DIST}" == "focal" ] || [ "${DIST}" == "hirsute" ] || [ "${DIST}" == "impish" ] || [ "${DIST}" == "jammy" ] || [ "${DIST}" == "buster" ] || [ "${DIST}" == "bullseye" ] || [ "${DIST}" == "bookworm" ]; then + if [ "${DIST}" == "bionic" ] || [ "${DIST}" == "focal" ] || [ "${DIST}" == "hirsute" ] || [ "${DIST}" == "impish" ] || [ "${DIST}" == "jammy" ] || [ "${DIST}" == "noble" ] || [ "${DIST}" == "buster" ] || [ "${DIST}" == "bullseye" ] || [ "${DIST}" == "bookworm" ]; then ln -sfT distros/ubuntu2004 debian elif [ "${DIST}" == "beowulf" ]; then ln -sfT distros/beowulf debian @@ -393,7 +393,7 @@ elif [ "${OS}" == "debian" ] || [ "${OS}" == "ubuntu" ] || [ "${OS}" == "raspbia execpackpack # Try to install and run the newly built zoneminder package - if [ "${OS}" == "ubuntu" ] && [ "${DIST}" == "bionic" ] && [ "${ARCH}" == "x86_64" ] && [ "${TRAVIS}" == "true" ]; then + if [ "${OS}" == "ubuntu" ] && [ "${DIST}" == "jammy" ] && [ "${ARCH}" == "x86_64" ] && [ "${TRAVIS}" == "true" ]; then echo "Begin Deb package installation..." install_deb fi diff --git a/version.txt b/version.txt index 276057b8cd..af02c90264 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.37.60 +1.37.63 diff --git a/web/ajax/status.php b/web/ajax/status.php index 6b9566c424..a2b274bf4e 100644 --- a/web/ajax/status.php +++ b/web/ajax/status.php @@ -440,6 +440,11 @@ function formatDateTime($dt) { case 'json' : { $response = array( strtolower(validJsStr($_REQUEST['entity'])) => $data ); + if ( ZM_OPT_USE_AUTH && (ZM_AUTH_RELAY == 'hashed') ) { + $auth_hash = generateAuthHash(ZM_AUTH_HASH_IPS); + $response['auth'] = $auth_hash; + $response['auth_relay'] = get_auth_relay(); + } if ( isset($_REQUEST['loopback']) ) $response['loopback'] = validJsStr($_REQUEST['loopback']); #ZM\Warning(print_r($response, true)); diff --git a/web/includes/Filter.php b/web/includes/Filter.php index 6f26c211c1..8e6418e229 100644 --- a/web/includes/Filter.php +++ b/web/includes/Filter.php @@ -30,6 +30,7 @@ class Filter extends ZM_Object { 'EmailSubject' => '', 'EmailBody' => '', 'EmailFormat' => 'Individual', + 'EmailServer' => '', 'AutoDelete' => 0, 'AutoArchive' => 0, 'AutoUnarchive' => 0, @@ -1120,7 +1121,7 @@ public function simple_widget() { if ( $term['attr'] == 'Archived' ) { $html .= ''; - $html .= htmlSelect("filter[Query][terms][$i][val]", $archiveTypes, $term['val'],['class'=>'chosen chosen-auto-width']).PHP_EOL; + $html .= htmlSelect("filter[Query][terms][$i][val]", $archiveTypes, $term['val'],['id'=>'filterArchived', 'class'=>'chosen chosen-auto-width']).PHP_EOL; $html .= ''; } else if ( $term['attr'] == 'Tags' ) { $selected = explode(',', $term['val']); @@ -1128,7 +1129,7 @@ public function simple_widget() { if (count($selected) == 1 and !$selected[0]) { $selected = null; } - $options = ['class'=>'chosen chosen-auto-width', 'multiple'=>'multiple', 'data-placeholder'=>translate('All Tags')]; + $options = ['id'=>'filterTags', 'class'=>'chosen chosen-auto-width', 'multiple'=>'multiple', 'data-placeholder'=>translate('All Tags')]; if (isset($term['cookie'])) { $options['data-cookie'] = $term['cookie']; @@ -1256,7 +1257,7 @@ public function simple_widget() { ['class'=>'term-value chosen chosen-auto-width', 'multiple'=>'multiple']).PHP_EOL; $html .= ''; } else if ( $term['attr'] == 'Notes' ) { - $attrs = ['class'=>'term-value chosen chosen-auto-width', 'multiple'=>'multiple', 'data-placeholder'=>translate('Event Type')]; + $attrs = ['id'=>'filterNotes', 'class'=>'term-value chosen chosen-auto-width', 'multiple'=>'multiple', 'data-placeholder'=>translate('Event Type')]; $selected = explode(',', $term['val']); if (count($selected) == 1 and !$selected[0]) { $selected = null; diff --git a/web/includes/FilterTerm.php b/web/includes/FilterTerm.php index d63123abdb..57eca402ab 100644 --- a/web/includes/FilterTerm.php +++ b/web/includes/FilterTerm.php @@ -386,7 +386,6 @@ public function hidden_fields() { } # end public function hiddens_fields public function test($event=null) { - Debug("Testing " . $this->attr); if ( !isset($event) ) { # Is a Pre Condition if ( $this->attr == 'DiskPercent' ) { diff --git a/web/includes/Monitor.php b/web/includes/Monitor.php index edaa60a50f..c73575631d 100644 --- a/web/includes/Monitor.php +++ b/web/includes/Monitor.php @@ -49,6 +49,10 @@ public static function getAnalysingOptions() { } return $AnalysingOptions; } +public static function getAnalysingString($option) { + $options = Monitor::getAnalysingOptions(); + return $options[$option]; +} protected static $AnalysisSourceOptions = null; public static function getAnalysisSourceOptions() { @@ -84,6 +88,11 @@ public static function getRecordingOptions() { return $RecordingOptions; } +public static function getRecordingString($option) { + $options = Monitor::getRecordingOptions(); + return $options[$option]; +} + protected static $RecordingSourceOptions = null; public static function getRecordingSourceOptions() { if (!isset($RecordingSourceOptions)) { @@ -114,17 +123,21 @@ public static function getDecodingOptions() { public static function getStatuses() { if (!isset($Statuses)) { $Statuses = array( - -1 => 'Unknown', - 0 => 'Idle', - 1 => 'PreAlarm', - 2 => 'Alarm', - 3 => 'Alert', - 4 => 'Tape' + 0 => 'Unknown', + 1 => 'Idle', + 2 => 'PreAlarm', + 3 => 'Alarm', + 4 => 'Alert', ); } return $Statuses; } +public static function getStateString($option) { + $statuses = Monitor::getStatuses(); + return $statuses[$option]; +} + protected static $table = 'Monitors'; protected $defaults = array( @@ -196,6 +209,7 @@ public static function getStatuses() { 'Encoder' => 'auto', 'OutputContainer' => null, 'EncoderParameters' => '', + 'WallClockTimestamps' => array('type'=>'boolean', 'default'=>0), 'RecordAudio' => array('type'=>'boolean', 'default'=>0), #'OutputSourceStream' => 'Primary', 'RTSPDescribe' => array('type'=>'boolean','default'=>0), @@ -264,6 +278,11 @@ public static function getStatuses() { 'AnalysisFPS' => null, 'CaptureFPS' => null, 'CaptureBandwidth' => null, + 'Capturing' => 0, + 'Analysing' => 0, + 'State' => 0, + 'LastEventId' => null, + 'EventId' => null, ); private $summary_fields = array( 'TotalEvents' => array('type'=>'integer', 'default'=>null, 'do_not_update'=>1), @@ -419,16 +438,8 @@ public function __call($fn, array $args) { return $this->defaults[$fn]; } else if (array_key_exists($fn, $this->status_fields)) { if ($this->Id()) { - $sql = 'SELECT * FROM `Monitor_Status` WHERE `MonitorId`=?'; - $row = dbFetchOne($sql, NULL, array($this->{'Id'})); - if (!$row) { - Warning('Unable to load Monitor status record for Id='.$this->{'Id'}.' using '.$sql); - } else { - foreach ($row as $k => $v) { - $this->{$k} = $v; - } - return $this->{$fn}; - } + $row = $this->Monitor_Status(); + if ($row) return $row[$fn]; } # end if this->Id return null; } else if (array_key_exists($fn, $this->summary_fields)) { @@ -451,6 +462,15 @@ public function __call($fn, array $args) { } } + public function Monitor_Status() { + if (!property_exists($this, 'Monitor_Status')) { + $sql = 'SELECT * FROM `Monitor_Status` WHERE `MonitorId`=?'; + $row = $this->{'Monitor_Status'} = dbFetchOne($sql, NULL, array($this->{'Id'})); + if (!$row) Warning('Unable to load Monitor status record for Id='.$this->{'Id'}.' using '.$sql); + } + return $this->{'Monitor_Status'}; + } + public function getStreamSrc($args, $querySep='&') { $streamSrc = $this->Server()->UrlToZMS( ZM_MIN_STREAMING_PORT ? @@ -491,8 +511,11 @@ public function getStreamSrc($args, $querySep='&') { if (isset($args['height'])) unset($args['height']); - $streamSrc .= '?'.http_build_query($args, '', $querySep); + unset($args['state']); + unset($args['zones']); + $streamSrc .= '?'.http_build_query($args, '', $querySep); + $this->streamSrc = $streamSrc; return $streamSrc; } // end function getStreamSrc @@ -945,7 +968,7 @@ function Manufacturer($new=-1) { function getMonitorStateHTML() { $html = '
-'.$this->Name().' +'.$this->Name().' (id='.$this->Id().')
'.translate('State').':'.$this->Status().' fps @@ -972,6 +995,8 @@ function getMonitorStateHTML() { * Same width height. If both are set we should calculate the smaller resulting scale */ function getStreamHTML($options) { + global $basename; + if (isset($options['scale']) and $options['scale'] != '' and $options['scale'] != 'fixed') { if ($options['scale'] != 'auto' && $options['scale'] != '0') { $options['width'] = reScale($this->ViewWidth(), $options['scale']).'px'; @@ -1021,12 +1046,15 @@ function getStreamHTML($options) { if ($this->StreamReplayBuffer()) $options['buffer'] = $this->StreamReplayBuffer(); //Warning("width: " . $options['width'] . ' height: ' . $options['height']. ' scale: ' . $options['scale'] ); + $blockRatioControl = ($basename == "montage") ? '' : ''; $html = '
- + ' . $blockRatioControl . '
-
+
Id()>1 ? $this->Id() : 0]); - $this->TimeUpdateStats = $dbStats[0]['TimeStamp']; - $this->CpuLoad = $dbStats[0]['CpuLoad']; - $this->CpuUserPercent = $dbStats[0]['CpuUserPercent']; - $this->CpuNicePercent = $dbStats[0]['CpuNicePercent']; - $this->CpuSystemPercent = $dbStats[0]['CpuSystemPercent']; - $this->CpuIdlePercent = $dbStats[0]['CpuIdlePercent']; - $this->CpuUsagePercent = $dbStats[0]['CpuUsagePercent']; - $this->TotalMem = $dbStats[0]['TotalMem']; - $this->FreeMem = $dbStats[0]['FreeMem']; - $this->TotalSwap = $dbStats[0]['TotalSwap']; - $this->FreeSwap = $dbStats[0]['FreeSwap']; + if (count($dbStats)) { + $this->TimeUpdateStats = $dbStats[0]['TimeStamp']; + $this->CpuLoad = $dbStats[0]['CpuLoad']; + $this->CpuUserPercent = $dbStats[0]['CpuUserPercent']; + $this->CpuNicePercent = $dbStats[0]['CpuNicePercent']; + $this->CpuSystemPercent = $dbStats[0]['CpuSystemPercent']; + $this->CpuIdlePercent = $dbStats[0]['CpuIdlePercent']; + $this->CpuUsagePercent = $dbStats[0]['CpuUsagePercent']; + $this->TotalMem = $dbStats[0]['TotalMem']; + $this->FreeMem = $dbStats[0]['FreeMem']; + $this->TotalSwap = $dbStats[0]['TotalSwap']; + $this->FreeSwap = $dbStats[0]['FreeSwap']; + } } public static function find( $parameters = array(), $options = array() ) { diff --git a/web/includes/actions/monitor.php b/web/includes/actions/monitor.php index e55cfce4eb..854fe04524 100644 --- a/web/includes/actions/monitor.php +++ b/web/includes/actions/monitor.php @@ -101,6 +101,7 @@ 'Exif' => 0, 'RTSPDescribe' => 0, 'V4LMultiBuffer' => '', + 'WallClockTimestamps' => '', 'RecordAudio' => 0, 'Method' => 'raw', 'GroupIds' => array(), diff --git a/web/includes/actions/montage.php b/web/includes/actions/montage.php index c4b55b2af6..37080a1d13 100644 --- a/web/includes/actions/montage.php +++ b/web/includes/actions/montage.php @@ -21,15 +21,15 @@ if ( isset($_REQUEST['object']) ) { if ( $_REQUEST['object'] == 'MontageLayout' ) { - if ($action == 'Save') { - $Layout = null; + $Layout = null; + if ($action == 'Save') { # Name is only populated when creating a new layout if ( $_REQUEST['Name'] != '' ) { $Layout = new ZM\MontageLayout(); $Layout->Name($_REQUEST['Name']); } else { - $Layout = new ZM\MontageLayout($_REQUEST['zmMontageLayout']); + $Layout = new ZM\MontageLayout(validCardinal($_REQUEST['zmMontageLayout'])); } if (canEdit('System') or !$Layout->Id() or ($user->Id() == $Layout->UserId())) { $Layout->UserId($user->Id()); @@ -44,7 +44,35 @@ ZM\Warning('Need System permissions to edit layouts'); return; } - } // end if save + } else if ($action == 'Delete') { // end if save + if ( isset($_REQUEST['zmMontageLayout']) ) { + $Layout = new ZM\MontageLayout(validCardinal($_REQUEST['zmMontageLayout'])); + } else { + ZM\Warning('Name of layout to be deleted is not specified'); + return; + } + + if (canEdit('System')) { + if ($Layout->Id()) { + $Layout->delete(); + zm_session_start(); + unset($_SESSION["zmMontageLayout"]); + $_SESSION['zmMontageLayout'] = ''; + session_write_close(); + unset($_COOKIE['zmMontageLayout']); + zm_setcookie('zmMontageLayout', '', array('expires'=>time()-3600*24)); //!!! After this JS still sees cookies, strange !!! + $redirect = '?view=montage'; + } else { + ZM\Warning('Layout Id=' . $_REQUEST['zmMontageLayout'] . ' not found for delete'); + $redirect = '?view=montage'; + } + } else { + ZM\Warning('Need System permissions to delete layouts'); + $redirect = '?view=montage'; + } + } else {// end if delete + ZM\Warning("Unsupported action $action in montage"); + } // end if else } # end if isset($_REQUEST['object'] ) } # end if isset($_REQUEST['object'] ) ?> diff --git a/web/includes/download_functions.php b/web/includes/download_functions.php index c921e771c6..7753968972 100644 --- a/web/includes/download_functions.php +++ b/web/includes/download_functions.php @@ -23,7 +23,7 @@ function exportEvents( $export_root, $exportFormat, $exportCompressed, - $exportStructure = false, + $exportStructure = false ) { if (!(canView('Events') or canView('Snapshots'))) { diff --git a/web/includes/functions.php b/web/includes/functions.php index cba59bc9dc..d27358133b 100644 --- a/web/includes/functions.php +++ b/web/includes/functions.php @@ -1007,6 +1007,8 @@ function parseFilter(&$filter, $saveToSession=false, $querySep='&') { $Filter = ZM\Filter::parse($filter, $querySep); + if (isset($filter['Id'])) + $filter['Id'] = validCardinal($filter['Id']); $filter['sql'] = $Filter->sql(); $filter['querystring'] = $Filter->querystring('filter', $querySep); $filter['hidden_fields'] = $Filter->hidden_fields(); diff --git a/web/includes/monitor_probe.php b/web/includes/monitor_probe.php index 6b46fc539f..28344f720f 100644 --- a/web/includes/monitor_probe.php +++ b/web/includes/monitor_probe.php @@ -835,7 +835,7 @@ function probeNetwork() { $cameras[$mac] = call_user_func('probe'.$macBase['type'], $ip, $username, $password); } } else { - ZM\Debug("No probe function for ${macBase['type']}"); + ZM\Debug("No probe function for {$macBase['type']}"); $cameras[$mac] = [['ip'=>$ip, 'Manufacturer'=>$macBase['vendor']]]; } } else { @@ -879,7 +879,7 @@ function probeNetwork() { } } else { $cameras[$mac] += [['ip'=>$ip, 'Manufacturer'=>$macBase['vendor']]]; - ZM\Debug("No probe function for ${macBase['type']} ${macBase['vendor']}"); + ZM\Debug("No probe function for {$macBase['type']} {$macBase['vendor']}"); } } else { ZM\Debug("No match for $macRoot"); diff --git a/web/includes/session.php b/web/includes/session.php index 127ca843d9..89ebc136b7 100644 --- a/web/includes/session.php +++ b/web/includes/session.php @@ -1,6 +1,9 @@ 100) newscale = 100; // we never request a larger image, as it just wastes bandwidth - if (newscale < 25) newscale = 25; // Arbitrary, lower values look bad + if (newscale < 25 && streamQuality > -1) newscale = 25; // Arbitrary, lower values look bad if (newscale <= 0) newscale = 100; this.scale = newscale; if (this.connKey) { @@ -198,7 +203,8 @@ function MonitorStream(monitorData) { console.log('No src on img?!', img); return; } - const newSrc = oldSrc.replace(/scale=\d+/i, 'scale='+newscale); + let newSrc = oldSrc.replace(/scale=\d+/i, 'scale='+newscale); + newSrc = newSrc.replace(/auth=\w+/i, 'auth='+auth_hash); if (newSrc != oldSrc) { this.streamCmdTimer = clearTimeout(this.streamCmdTimer); // We know that only the first zms will get the command because the @@ -236,21 +242,21 @@ function MonitorStream(monitorData) { }}); } attachVideo(parseInt(this.id), this.janusPin); - this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), delay); + this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout); return; } if (this.RTSP2WebEnabled) { if (ZM_RTSP2WEB_PATH) { - videoEl = document.getElementById("liveStream" + this.id); + const videoEl = document.getElementById("liveStream" + this.id); const url = new URL(ZM_RTSP2WEB_PATH); const useSSL = (url.protocol == 'https'); - rtsp2webModUrl = url; + const rtsp2webModUrl = url; rtsp2webModUrl.username = ''; rtsp2webModUrl.password = ''; //.urlParts.length > 1 ? urlParts[1] : urlParts[0]; // drop the username and password for viewing if (this.RTSP2WebType == 'HLS') { - hlsUrl = rtsp2webModUrl; + const hlsUrl = rtsp2webModUrl; hlsUrl.pathname = "/stream/" + this.id + "/channel/0/hls/live/index.m3u8"; /* if (useSSL) { @@ -273,18 +279,16 @@ function MonitorStream(monitorData) { videoEl.play(); } }); - mseUrl = rtsp2webModUrl; + const mseUrl = rtsp2webModUrl; mseUrl.protocol = useSSL ? 'wss' : 'ws'; mseUrl.pathname = "/stream/" + this.id + "/channel/0/mse?uuid=" + this.id + "&channel=0"; - console.log(mseUrl.href); startMsePlay(this, videoEl, mseUrl.href); } else if (this.RTSP2WebType == 'WebRTC') { - webrtcUrl = rtsp2webModUrl; + const webrtcUrl = rtsp2webModUrl; webrtcUrl.pathname = "/stream/" + this.id + "/channel/0/webrtc"; - console.log(webrtcUrl.href); startRTSP2WebPlay(videoEl, webrtcUrl.href); } - this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), delay); + this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout); return; } else { console.log("ZM_RTSP2WEB_PATH is empty. Go to Options->System and set ZM_RTSP2WEB_PATH accordingly."); @@ -305,7 +309,8 @@ function MonitorStream(monitorData) { if (stream.getAttribute('loading') == 'lazy') { stream.setAttribute('loading', 'eager'); } - src = stream.src.replace(/mode=single/i, 'mode=jpeg'); + let src = stream.src.replace(/mode=single/i, 'mode=jpeg'); + src = src.replace(/auth=\w+/i, 'auth='+auth_hash); if (-1 == src.search('connkey')) { src += '&connkey='+this.connKey; } @@ -316,13 +321,14 @@ function MonitorStream(monitorData) { } stream.onerror = this.img_onerror.bind(this); stream.onload = this.img_onload.bind(this); + this.started = true; }; // this.start this.stop = function() { if ( 0 ) { const stream = this.getElement(); if (!stream) return; - src = stream.src.replace(/mode=jpeg/i, 'mode=single'); + const src = stream.src.replace(/mode=jpeg/i, 'mode=single'); if (stream.src != src) { stream.src = ''; stream.src = src; @@ -331,6 +337,7 @@ function MonitorStream(monitorData) { this.streamCommand(CMD_STOP); this.statusCmdTimer = clearInterval(this.statusCmdTimer); this.streamCmdTimer = clearInterval(this.streamCmdTimer); + this.started = false; }; this.kill = function() { @@ -340,18 +347,29 @@ function MonitorStream(monitorData) { } } const stream = this.getElement(); - if (!stream) return; + if (!stream) { + console.log("No element found for monitor "+this.id); + return; + } stream.onerror = null; stream.onload = null; this.stop(); - if (this.ajaxQueue) { + // this.stop tells zms to stop streaming, but the process remains. We need to turn the stream into an image. + if (stream.src) { + const src = stream.src.replace(/mode=jpeg/i, 'mode=single'); + if (stream.src != src) { + stream.src = ''; + stream.src = src; + } + } + + // Because we stopped the zms process above, any remaining ajaxes will fail. But aborting them will also cause them to fail, so why bother? + if (0 && this.ajaxQueue) { console.log("Aborting in progress ajax for kill"); // Doing this for responsiveness, but we could be aborting something important. Need smarter logic this.ajaxQueue.abort(); } - this.statusCmdTimer = clearInterval(this.statusCmdTimer); - this.streamCmdTimer = clearInterval(this.streamCmdTimer); }; this.pause = function() { @@ -508,7 +526,7 @@ function MonitorStream(monitorData) { const captureFPSValue = $j('#captureFPSValue'+this.id); const analysisFPSValue = $j('#analysisFPSValue'+this.id); - this.status.fps = this.status.fps.toLocaleString(undefined, {minimumFractionDigits: 1, maximumFractionDigits: 1}); + this.status.fps = this.status.fps.toLocaleString(undefined, {minimumFractionDigits: 1, maximumFractionDigits: 2}); if (viewingFPSValue.length && (viewingFPSValue.text != this.status.fps)) { viewingFPSValue.text(this.status.fps); } @@ -631,10 +649,11 @@ function MonitorStream(monitorData) { } // end if canEdit.Monitors if (this.status.auth) { - if (this.status.auth != this.auth_hash) { + if (this.status.auth != auth_hash) { // Don't reload the stream because it causes annoying flickering. Wait until the stream breaks. - console.log("Changed auth from " + this.auth_hash + " to " + this.status.auth); - this.streamCmdParms.auth = auth_hash = this.auth_hash = this.status.auth; + console.log("Changed auth from " + auth_hash + " to " + this.status.auth); + auth_hash = this.status.auth; + auth_relay = this.status.auth_relay; } } // end if have a new auth hash } // end if has state @@ -643,8 +662,8 @@ function MonitorStream(monitorData) { // Try to reload the image stream. if (stream.src) { console.log('Reloading stream: ' + stream.src); - src = stream.src.replace(/rand=\d+/i, 'rand='+Math.floor((Math.random() * 1000000) )); - src = src.replace(/auth=\w+/i, 'auth='+this.auth_hash); + let src = stream.src.replace(/rand=\d+/i, 'rand='+Math.floor((Math.random() * 1000000) )); + src = src.replace(/auth=\w+/i, 'auth='+auth_hash); // Maybe updated auth if (src != stream.src) { stream.src = ''; @@ -667,11 +686,11 @@ function MonitorStream(monitorData) { const monitor = respObj.monitor; if (monitor.FrameRate) { - const fpses = monitor.FrameRate.split(","); + const fpses = monitor.FrameRate.split(','); fpses.forEach(function(fps) { const name_values = fps.split(':'); const name = name_values[0].trim(); - const value = name_values[1].trim().toLocaleString(undefined, {minimumFractionDigits: 1, maximumFractionDigits: 1}); + const value = name_values[1].trim().toLocaleString(undefined, {minimumFractionDigits: 1, maximumFractionDigits: 2}); if (name == 'analysis') { this.status.analysisfps = value; @@ -735,13 +754,22 @@ function MonitorStream(monitorData) { } // end if canEdit.Monitors this.setAlarmState(monitorStatus); + + if (respObj.auth_hash) { + if (auth_hash != respObj.auth_hash) { + // Don't reload the stream because it causes annoying flickering. Wait until the stream breaks. + console.log("Changed auth from " + auth_hash + " to " + respObj.auth_hash); + auth_hash = respObj.auth_hash; + auth_relay = respObj.auth_relay; + } + } // end if have a new auth hash } else { checkStreamForErrors('getStatusCmdResponse', respObj); } }; // this.getStatusCmdResponse - this.statusCmdQuery=function() { - $j.getJSON(this.url + '?view=request&request=status&entity=monitor&element[]=Status&element[]=CaptureFPS&element[]=AnalysisFPS&element[]=Analysing&element[]=Recording&id='+this.id+'&'+this.auth_relay) + this.statusCmdQuery = function() { + $j.getJSON(this.url + '?view=request&request=status&entity=monitor&element[]=Status&element[]=CaptureFPS&element[]=AnalysisFPS&element[]=Analysing&element[]=Recording&id='+this.id+'&'+auth_relay) .done(this.getStatusCmdResponse.bind(this)) .fail(logAjaxFail); }; @@ -777,7 +805,7 @@ function MonitorStream(monitorData) { this.alarmCommand = function(command) { if (this.ajaxQueue) { - console.log("Aborting in progress ajax for alarm"); + console.log('Aborting in progress ajax for alarm'); // Doing this for responsiveness, but we could be aborting something important. Need smarter logic this.ajaxQueue.abort(); } @@ -790,7 +818,7 @@ function MonitorStream(monitorData) { url: this.url + (auth_relay?'?'+auth_relay:''), xhrFields: {withCredentials: true}, data: alarmCmdParms, - dataType: "json" + dataType: 'json' }) .done(this.getStreamCmdResponse.bind(this)) .fail(this.onFailure.bind(this)); @@ -804,7 +832,7 @@ function MonitorStream(monitorData) { url: this.url + (auth_relay?'?'+auth_relay:''), xhrFields: {withCredentials: true}, data: streamCmdParms, - dataType: "json" + dataType: 'json' }) .done(this.getStreamCmdResponse.bind(this)) .fail(this.onFailure.bind(this)); @@ -819,7 +847,7 @@ function MonitorStream(monitorData) { this.setMaxFPS = function(maxfps) { if (1) { - this.streamCommand({command: CMD_MAXFPS, maxfps: currentSpeed}); + this.streamCommand({command: CMD_MAXFPS, maxfps: maxfps}); } else { var streamImage = this.getElement(); const oldsrc = streamImage.attr('src'); diff --git a/web/js/panzoom.js b/web/js/panzoom.js new file mode 100644 index 0000000000..d8034ad830 --- /dev/null +++ b/web/js/panzoom.js @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2024 ZoneMinder + * This file is for managing jquery.panzoom.js + */ + +var zmPanZoom = { + panZoomMaxScale: 10, + panZoomStep: 0.3, + panZoom: [], + shifted: null, + ctrled: null, + alted: null, + panOnlyWhenZoomed: true, + //canvas: true, + touchAction: 'manipulation', + /* + * param.objString - class or id + */ + init: function(params={}) { + if (!panZoomEnabled) return; + const _this = this; + const object = (params.objString) ? $j(params.objString) : $j('.zoompan'); + + object.each( function() { + params.obj = this; + _this.action('enable', params); + }); + }, + + /* + * params['obj'] : DOM object + * params['id'] : monitor id + * params['contain'] : "inside" | "outside", default="outside" + * params['disablePan'] : true || false + * & etc + */ + action: function(action, params) { + const _this = this; + const objString = params['objString']; + const contain = (params['contain']) ? params['contain'] : "outside"; + const minScale = (contain != "outside") ? 0.1 : 1.0; + if (action == "enable") { + var id; + + if (typeof eventData != 'undefined') { + id = eventData.MonitorId; //Event page + } else { + const obj = this.getStream(params['obj']); + + if (obj.length > 0) { + id = stringToNumber(obj[0].id); //Montage & Watch page + } + } + if (!id) { + console.log("The for panZoom action object was not found.", params); + return; + } + $j('.btn-zoom-in').removeClass('hidden'); + $j('.btn-zoom-out').removeClass('hidden'); + const objPanZoom = (params['additional'] && objString) ? id+objString : id; + + // default value for ZM, if not explicitly specified in the parameters + if (!('contain' in params)) params.contain = contain; + if (!('minScale' in params)) params.minScale = minScale; + if (!('maxScale' in params)) params.maxScale = params['additional'] ? 1 : this.panZoomMaxScale; + if (!('step' in params)) params.step = this.panZoomStep; + if (!('cursor' in params)) params.cursor = 'inherit'; + if (!('disablePan' in params)) params.disablePan = false; + if (!('roundPixels' in params)) params.roundPixels = false; + if (!('panOnlyWhenZoomed' in params)) params.panOnlyWhenZoomed = this.panOnlyWhenZoomed; + //if (!('canvas' in params)) params.canvas = this.canvas; + if (!('touchAction' in params)) params.touchAction = this.touchAction; + + //Direct initialization Panzoom + this.panZoom[objPanZoom] = Panzoom(params['obj'], params); + this.panZoom[objPanZoom].target = params['obj']; + this.panZoom[objPanZoom].additional = params['additional']; + //panZoom[id].pan(10, 10); + //panZoom[id].zoom(1, {animate: true}); + // Binds to shift || alt + wheel + params['obj'].parentElement.addEventListener('wheel', function(event) { + if (!_this.shifted && !_this.alted) { + return; + } + + if (_this.shifted && _this.alted) { + event.preventDefault(); //Avoid page scrolling + if (!_this.panZoom[objPanZoom].additional) return; + + const obj = (event.target.closest('#monitor'+id)) ? event.target.closest('#monitor'+id)/*Watch & Montage*/ : event.target.closest('#eventVideo')/*Event*/; + const objDim = obj.getBoundingClientRect(); + //Get the coordinates (x - the middle of the image, y - the top edge) of the point relative to which we will scale the image + const x = objDim.x + objDim.width/2; + const y = objDim.y; + const scale = (event.deltaY < 0) ? _this.panZoom[objPanZoom].getScale() * Math.exp(_this.panZoomStep)/*scrolling up*/ : _this.panZoom[objPanZoom].getScale() / Math.exp(_this.panZoomStep)/*scrolling down*/; + _this.panZoom[objPanZoom].zoomToPoint(scale, {clientX: x, clientY: y}); + } else if (_this.shifted && !_this.alted) { + if (!_this.panZoom[id]) return; + _this.panZoom[id].zoomWithWheel(event); + } else { + return; + } + _this.setTriggerChangedMonitors(id); + }); + + $j(document).on('keyup.panzoom keydown.panzoom', function(e) { + _this.shifted = e.shiftKey ? e.shiftKey : e.shift; + _this.ctrled = e.ctrlKey; + _this.alted = e.altKey; + _this.manageCursor(id); + }); + + params['obj'].addEventListener('mousemove', handlePanZoomEventMousemove); + params['obj'].addEventListener('panzoomchange', handlePanZoomEventPanzoomchange); + params['obj'].addEventListener('panzoomzoom', handlePanZoomEventPanzoomzoom); + params['obj'].addEventListener('panzoomstart', handlePanZoomEventPanzoomstart); + params['obj'].addEventListener('panzoompan', handlePanzoompan); + params['obj'].addEventListener('panzoomend', handlePanzoomend); + params['obj'].addEventListener('panzoomreset', handlePanzoomreset); + } else if (action == "disable") { //Disable a specific object + if (!this.panZoom[params['id']]) { + console.log(`PanZoom for monitor "${params['id']}" was not initialized.`); + return; + } + //Disables for the entire document!//$j(document).off('keyup.panzoom keydown.panzoom'); + const obj = this.panZoom[params['id']].target; + // #videoFeed - Event page, #monitorX - Montage & Watch page + const el = document.getElementById('videoFeed'); + const wrapper = el ? el : document.getElementById('monitor'+params['id']); + + $j(wrapper).find('.btn-zoom-in, .btn-zoom-out').addClass('hidden'); + this.panZoom[params['id']].reset(); + this.panZoom[params['id']].resetStyle(); + this.panZoom[params['id']].setOptions({disablePan: true, disableZoom: true}); + this.panZoom[params['id']].destroy(); + obj.removeEventListener('panzoomzoom', handlePanZoomEventPanzoomzoom); + obj.removeEventListener('mousemove', handlePanZoomEventMousemove); + obj.removeEventListener('panzoomchange', handlePanZoomEventPanzoomchange); + obj.removeEventListener('panzoomzoom', handlePanZoomEventPanzoomzoom); + obj.removeEventListener('panzoomstart', handlePanZoomEventPanzoomstart); + obj.removeEventListener('panzoompan', handlePanzoompan); + obj.removeEventListener('panzoomend', handlePanzoomend); + obj.removeEventListener('panzoomreset', handlePanzoomreset); + } + }, + + zoomIn: function(clickedElement) { + if (clickedElement.target.id) { + var id = stringToNumber(clickedElement.target.id); + } else { //There may be an element without ID inside the button + var id = stringToNumber(clickedElement.target.parentElement.id); + } + if (clickedElement.ctrlKey) { + // Double the zoom step. + this.panZoom[id].zoom(this.panZoom[id].getScale() * Math.exp(this.panZoomStep*2), {animate: true}); + } else { + this.panZoom[id].zoomIn(); + } + this.setTriggerChangedMonitors(id); + this.setTouchAction(this.panZoom[id]); + this.manageCursor(id); + }, + + zoomOut: function(clickedElement) { + const id = stringToNumber(clickedElement.target.id ? clickedElement.target.id : clickedElement.target.parentElement.id); + if (clickedElement.ctrlKey) { + // Reset zoom + this.panZoom[id].zoom(1, {animate: true}); + } else { + this.panZoom[id].zoomOut(); + } + this.setTriggerChangedMonitors(id); + this.setTouchAction(this.panZoom[id]); + this.manageCursor(id); + }, + + setTouchAction: function(el) { + const currentScale = el.getScale().toFixed(1); + if (currentScale == 1) { + el.setOptions({touchAction: 'manipulation'}); + } else { + el.setOptions({touchAction: 'none'}); + } + }, + + /* + * id - Monitor ID + * !!! On Montage & Watch page, when you hover over a block of buttons (in the empty space between the buttons themselves), the cursor changes, but no action occurs, you need to review "monitors[i]||monitorStream.setup_onclick(handleClick)" + */ + manageCursor: function(id) { + if (!this.panZoom[id]) { + console.log(`PanZoom for monitor ID=${id} was not initialized.`); + return; + } + var obj; + var obj_btn; + const disablePan = this.panZoom[id].getOptions().disablePan; + const disableZoom = this.panZoom[id].getOptions().disableZoom; + + obj = this.getStream(id); + if (obj) { //Montage & Watch page + obj_btn = document.getElementById('button_zoom'+id); //Change the cursor when you hover over the block of buttons at the top of the image. Not required on Event page + } else { //Event page + obj = document.getElementById('videoFeedStream'+id); + } + + if (!obj) { + console.log(`Stream with id=${id} not found.`); + return; + } + const currentScale = this.panZoom[id].getScale().toFixed(1); + + if (this.shifted && this.ctrled) { + const cursor = (disableZoom) ? 'auto' : 'zoom-out'; + obj.style['cursor'] = cursor; + if (obj_btn) { + obj_btn.style['cursor'] = cursor; + } + } else if (this.shifted) { + const cursor = (disableZoom) ? 'auto' : 'zoom-in'; + obj.style['cursor'] = cursor; + if (obj_btn) { + obj_btn.style['cursor'] = cursor; + } + } else if (this.ctrled) { + if (currentScale == 1.0) { + obj.style['cursor'] = 'auto'; + if (obj_btn) { + obj_btn.style['cursor'] = 'auto'; + } + } else { + const cursor = (disableZoom) ? 'auto' : 'zoom-out'; + obj.style['cursor'] = cursor; + if (obj_btn) { + obj_btn.style['cursor'] = cursor; + } + } + } else { //No ctrled & no shifted + if (currentScale == 1.0) { + obj.style['cursor'] = 'auto'; + if (obj_btn) { + obj_btn.style['cursor'] = 'auto'; + } + } else { + const cursor = (disablePan) ? 'auto' : 'move'; + obj.style['cursor'] = cursor; + if (obj_btn) { + obj_btn.style['cursor'] = cursor; + } + } + } + }, + + click: function(id) { + if (this.ctrled && this.shifted) { + this.panZoom[id].zoom(1, {animate: true}); + } else if (this.ctrled) { + this.panZoom[id].zoomOut(); + } else if (this.shifted) { + const scale = this.panZoom[id].getScale() * Math.exp(this.panZoomStep); + const point = {clientX: event.clientX, clientY: event.clientY}; + this.panZoom[id].zoomToPoint(scale, point, {focal: {x: event.clientX, y: event.clientY}}); + } + if (this.ctrled || this.shifted) { + this.setTriggerChangedMonitors(id); + } + this.setTouchAction(this.panZoom[id]); + }, + + getStream: function(id) { + if (isNaN(id)) { + const liveStream = $j(id).find('[id ^= "liveStream"]'); + const evtStream = $j(id).find('[id ^= "evtStream"]'); + return (liveStream.length > 0) ? liveStream : evtStream; + } else { + const liveStream = document.getElementById('liveStream'+id); + const evtStream = document.getElementById('evtStream'+id); + return (liveStream) ? liveStream : evtStream; + } + }, + + setTriggerChangedMonitors: function(id) { + if (typeof setTriggerChangedMonitors !== 'undefined' && $j.isFunction(setTriggerChangedMonitors)) { + //Montage page + setTriggerChangedMonitors(id); + } else { + // Event page + updateScale = true; + } + } +}; + +function handlePanZoomEventMousemove(event) { + if (typeof panZoomEventMousemove !== 'undefined' && $j.isFunction(panZoomEventMousemove)) panZoomEventMousemove(event); +} + +function handlePanZoomEventPanzoomchange(event) { + if (typeof panZoomEventPanzoomchange !== 'undefined' && $j.isFunction(panZoomEventPanzoomchange)) panZoomEventPanzoomchange(event); + //console.log('panzoomchange', event.detail) // => { x: 0, y: 0, scale: 1 } +} + +function handlePanZoomEventPanzoomzoom(event) { + if (typeof panZoomEventPanzoomzoom !== 'undefined' && $j.isFunction(panZoomEventPanzoomzoom)) panZoomEventPanzoomzoom(event); +} + +function handlePanZoomEventPanzoomstart(event) { + if (typeof panZoomEventPanzoomstart !== 'undefined' && $j.isFunction(panZoomEventPanzoomstart)) panZoomEventPanzoomstart(event); +} + +function handlePanzoompan(event) { + if (typeof panZoomEventPanzoompan !== 'undefined' && $j.isFunction(panZoomEventPanzoompan)) panZoomEventPanzoompan(event); +} + +function handlePanzoomend(event) { + if (typeof panZoomEventPanzoomend !== 'undefined' && $j.isFunction(panZoomEventPanzoomend)) panZoomEventPanzoomend(event); +} + +function handlePanzoomreset(event) { + if (typeof panZoomEventPanzoomreset !== 'undefined' && $j.isFunction(panZoomEventPanzoomreset)) panZoomEventPanzoomreset(event); +} diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index 3a7b073438..2cf8f597a8 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -220,6 +220,7 @@ 'ConfirmDeleteControl' => 'Warning, deleting a control will reset all monitors that use it to be uncontrollable.

Are you sure you wish to delete?', 'ConfirmDeleteDevices' => 'Are you sure you wish to delete the selected devices?', 'ConfirmDeleteEvents' => 'Are you sure you wish to delete the selected events?', + 'ConfirmDeleteLayout' => 'Are you sure you wish to delete current layout?', 'ConfirmDeleteTitle' => 'Delete Confirmation', 'ConfirmPassword' => 'Confirm Password', 'ConfirmUnarchiveEvents'=> 'Are you sure you wish to unarchive the selected events?', @@ -277,6 +278,7 @@ 'Exif' => 'Embed EXIF data into image', 'DownloadVideo' => 'Download Video', 'GenerateDownload' => 'Generate Download', + 'EventsLoading' => 'Events are loading', 'ExistsInFileSystem' => 'Exists In File System', 'ExportFailed' => 'Export Failed', 'ExportFormat' => 'Export File Format', @@ -600,6 +602,7 @@ 'TimestampLabelY' => 'Timestamp Label Y', 'TimestampLabelSize' => 'Font Size', 'TimeStamp' => 'Time Stamp', + 'TooManyEventsForTimeline' => 'Too many events for Timeline. Reduce the number of monitors or reduce the visible range of the Timeline', 'TotalBrScore' => 'Total
Score', 'TrackDelay' => 'Track Delay', 'TrackMotion' => 'Track Motion', diff --git a/web/lang/fr_fr.php b/web/lang/fr_fr.php index a5a0453b41..2966b787f8 100644 --- a/web/lang/fr_fr.php +++ b/web/lang/fr_fr.php @@ -226,6 +226,7 @@ 'Config' => 'Config', 'ConfiguredFor' => 'Configuré pour', 'ConfirmDeleteEvents' => 'Etes-vous sûr de vouloir effacer le(s) événement(s) sélectionné(s)?', + 'ConfirmDeleteLayout' => 'Êtes-vous sûr de vouloir supprimer la mise en page actuelle ?', 'ConfirmPassword' => 'Répéter mot de passe', 'ConjAnd' => 'et', 'ConjOr' => 'ou', diff --git a/web/lang/ja_jp.php b/web/lang/ja_jp.php index 101b9c3e6f..fa05b2156a 100644 --- a/web/lang/ja_jp.php +++ b/web/lang/ja_jp.php @@ -51,7 +51,7 @@ // // Example // header( "Content-Type: text/html; charset=iso-8859-1" ); -header( "Content-Type: text/html; charset=Shift_JIS" ); +header( "Content-Type: text/html; charset=UTF-8" ); // You may need to change your locale here if your default one is incorrect for the // language described in this file, or if you have multiple languages supported. diff --git a/web/lang/ru_ru.php b/web/lang/ru_ru.php index 55d2b42649..79c43473ae 100644 --- a/web/lang/ru_ru.php +++ b/web/lang/ru_ru.php @@ -246,6 +246,7 @@ 'Config' => 'Конфигурация', 'ConfiguredFor' => 'настроен на', 'ConfirmDeleteEvents' => 'Вы действительно хотите удалить выбранные события?', + 'ConfirmDeleteLayout' => 'Вы действительно хотите удалить текущий шаблон?', 'ConfirmPassword' => 'Подтвердите пароль', 'ConfirmUnarchiveEvents'=> 'Вы уверены, что хотите удалить из архива выбранные события?', 'ConjAnd' => 'и', @@ -320,6 +321,8 @@ 'EventName' => 'Имя события', // Added - 2019-03-25 'EventPrefix' => 'Префикс события', 'Events' => 'События', + 'events' => 'событий', // Added - 2024-07-16 + 'EventsLoading' => 'Идет загрузка событий', 'Exclude' => 'Исключить', 'Execute' => 'Выполнить', 'Exif' => 'Включить EXIF информацию в изображение', // Added - 2019-03-24 @@ -511,6 +514,7 @@ 'MonitorPresetIntro' => 'Выберите подходящий вариант из списка ниже.

Обратите внимание, что это может переписать настройки определенные для этого монитора.

', 'MonitorProbe' => 'Поиск камеры', // Added - 2009-03-31 'MonitorProbeIntro' => 'В этом списке показаны найденные аналоговые и сетевые камеры, как уже заведенные, так и доступные для выбора.

Выберите нужную из списка ниже.

Обратите внимание, что не все камеры могут быть найдены, и что выбор камеры может переписать настройки определенные для этого монитора.

', // Added - 2009-03-31 + 'Monitor status position' => 'Положение статуса монитора', 'Monitors' => 'Мониторы', 'Montage' => 'Монтаж', 'MontageReview' => 'Обзор монтажа', // Added - 2019-03-24 @@ -612,6 +616,7 @@ 'RTSPDescribe' => 'Использовать RTSP URL для ответа', // Edited - 2019-03-25 'RTSPTransport' => 'Транспортный протокол RTSP', // Edited - 2019-03-25 'Rate' => 'Скорость', + 'Ratio' => 'Соотношение', 'Real' => 'Реальная', 'RecaptchaWarning' => 'Ваш секретный ключ reCAPTCHA недействителен. Пожалуйста, исправьте это, или reCAPTCHA не будет работать', // Added - 2018-08-30 'Record' => 'Запись', @@ -707,9 +712,11 @@ 'StorageScheme' => 'Схема', // Added - 2018-08-30 'Stream' => 'Поток', 'StreamReplayBuffer' => 'Буфер потока повторного воспр.', + 'Stream quality' => 'Качество потока', 'Submit' => 'Применить', 'System' => 'Система', 'SystemLog' => 'Лог системы', // Added - 2011-06-16 + 'Tags' => 'Теги', 'TargetColorspace' => 'Цветовое пространство', // Added - 2015-04-18 'Tele' => 'Теле', 'Thumbnail' => 'Миниатюра', @@ -729,6 +736,7 @@ 'TimestampLabelY' => 'Y-координата метки', 'Today' => 'Сегодня', 'Tools' => 'Инструменты', + 'TooManyEventsForTimeline' => 'Слишком много событий для шкалы времени. Уменьшите количество мониторов или уменьшите видимый диапазон шкалы', 'Total' => 'Всего', // Added - 2011-06-16 'TotalBrScore' => 'Сумм.
оценка', 'TrackDelay' => 'Задержка обнаружения', @@ -840,8 +848,9 @@ 'API Enabled' => 'API включен', 'CpuLoad' => 'Загрузка процессора', 'Actions' => 'Действия', - '8 Hour' => '8 Часов', - '1 Hour' => '1 Час', + '24 Hour' => '24 часа', + '8 Hour' => '8 часов', + '1 Hour' => '1 час', 'Scale' => 'Шкала', // Montage Review -> type button 'All Events' => 'Все события', 'HISTORY' => 'История', // Montage Review -> type button -> js @@ -857,7 +866,8 @@ 'ZeroSize' => 'Нулевой размер', 'MinGap' => 'Минимальный разрыв', 'MaxGap' => 'Максимальный разрыв', - 'Event Start Time' => 'Время начала события', + 'Event Start Time' => 'Время начала события', + 'Start Time' => 'Время старта', 'to' => 'до', 'Accept' => 'Принять', 'Decline' => 'Отклонить', @@ -927,7 +937,7 @@ 'Prealarm' => 'Предтревожный', 'Settings only available for Local monitors.' => 'Настройки доступны только для локальных мониторов.', 'KeyFrames Only' => 'Только ключевые кадры', - 'Keyframes + Ondemand' => 'Ключевые кадры + по требованию', + 'Keyframes + Ondemand' => 'Ключевые кадры + по требованию', 'On Motion / Trigger / etc' => 'При движении / срабатывании / и т.д', 'Less important' => 'Менее важный', 'Not important' => 'Не важно', @@ -939,7 +949,7 @@ 'Analysis images only (if available)' => 'Только аналитические изображения (если есть)', 'Frames + Analysis images (if available)' => 'Кадры + Аналитические изображения (если есть)', 'Full Colour' => 'Полноцветный', - 'Y-Channel (Greyscale)' => 'Y-Канал (оттенки серого)', + 'Y-Channel (Greyscale)'=> 'Y-Канал (оттенки серого)', 'Linear' => 'Линейный', 'Discard' => 'Отбрасывать', 'Blend' => 'Смешивание', @@ -958,19 +968,25 @@ 'Showing Analysis' => 'Отображение анализа', 'ConfirmDeleteTitle' => 'Подтвердите удаление заголовка', 'Continuous' => 'Непрерывный', - 'ONVIF_Alarm_Text' => 'Текст сигнала тревоги ONVIF', //added 18/07/2022 - 'None' => 'Нет', + 'ONVIF_Alarm_Text' => 'Текст сигнала тревоги ONVIF', //added 18/07/2022 + 'None' => 'Нет', 'Free' => 'Свободно', 'RunStats' => 'Текущие показатели', 'RunAudit' => 'Запуск аудита', 'RunTrigger' => 'Запуск триггера', - 'RunEventNotification' => 'Запустить уведомление о событии', + 'RunEventNotification' => 'Запустить уведомление о событии', 'normal' => 'нормальный', 'Path' => 'Путь', 'Snapshots' => 'Снапшоты', /******************* 27-02-24 **********************************/ 'ONVIF_EVENTS_PATH' => 'Путь к ONVIF событиям', - 'SOAP WSA COMPLIANCE' => 'Совместимость с SOAP WSA', + 'SOAP WSA COMPLIANCE' => 'Совместимость с SOAP WSA', + +/******************* 16-07-24 Montage page ***************************/ + 'Inside bottom' => 'Внизу внутри', + 'Outside bottom' => 'Внизу снаружи', + 'Hidden' => 'Скрыт', + 'Show on hover' => 'При наведении', /******************* Название языков 27-02-24 **********************************/ 'es_la' => 'Испанский Латинская Америка', diff --git a/web/skins/classic/assets/jquery.panzoom/dist/jquery.panzoom.js b/web/skins/classic/assets/jquery.panzoom/dist/jquery.panzoom.js index 65b35d53fa..b692ca1532 100644 --- a/web/skins/classic/assets/jquery.panzoom/dist/jquery.panzoom.js +++ b/web/skins/classic/assets/jquery.panzoom/dist/jquery.panzoom.js @@ -573,6 +573,7 @@ if (!opts.force && opts.disableZoom) { return; } + const toScalePlanned = toScale; toScale = result.scale; var toX = x; var toY = y; @@ -581,8 +582,8 @@ // plus the current translation after the scale // neutralized to no scale (as the transform scale will apply to the translation) var focal = opts.focal; - toX = (focal.x / toScale - focal.x / scale + x * toScale) / toScale; - toY = (focal.y / toScale - focal.y / scale + y * toScale) / toScale; + toX = (focal.x / toScale - focal.x / scale + x * toScalePlanned) / toScalePlanned; + toY = (focal.y / toScale - focal.y / scale + y * toScalePlanned) / toScalePlanned; } var panResult = constrainXY(toX, toY, toScale, { relative: false, force: true }); x = panResult.x; diff --git a/web/skins/classic/css/base/skin.css b/web/skins/classic/css/base/skin.css index a4dad51e62..dff9f6f51d 100644 --- a/web/skins/classic/css/base/skin.css +++ b/web/skins/classic/css/base/skin.css @@ -24,6 +24,8 @@ :root { --scrollbarBG: #F1F1F1; --sliderBG: #C1C1C1; + --alarmBG: #FFC0C0; + --alarmText: inherit; } @font-face { @@ -467,18 +469,6 @@ body.sticky #content { * Generic useful classes, especially with mootools */ -.hidden { - display: none; -} - -.hidden-shift { - position: absolute !important; left: -999em !important; -} - -.invisible { - visibility: hidden; -} - .nowrap { white-space: nowrap; } @@ -678,7 +668,7 @@ input[type=button]:disabled, input[type=submit]:disabled, a.disabled, a.btn-primary.disabled, -.btn-primary:disabled { +.btn-primary.disabled, .btn-primary:disabled, .btn-secondary.disabled, .btn-secondary:disabled { background-color: #aaaaaa; border-color: #bbbbbb; } @@ -936,6 +926,11 @@ a.flip { clear: both; } +.monitor .monitorStatus { + position: relative; + background-color: #FFFFFF; +} + .monitor .monitorStatus.bottom { color: #dddddd; } @@ -1074,16 +1069,16 @@ div::-webkit-scrollbar, nav::-webkit-scrollbar, .chosen-results::-webkit-scrollb height: 11px; } -div, nav, .chosen-results { +html, div, nav, .chosen-results { scrollbar-width: thin; scrollbar-color: var(--sliderBG) var(--scrollbarBG); } -div::-webkit-scrollbar-track, nav::-webkit-scrollbar-track, .chosen-results::-webkit-scrollbar-track { +html::-webkit-scrollbar-track, div::-webkit-scrollbar-track, nav::-webkit-scrollbar-track, .chosen-results::-webkit-scrollbar-track { background: var(--scrollbarBG); } -div::-webkit-scrollbar-thumb, nav::-webkit-scrollbar-thumb, .chosen-results::-webkit-scrollbar-thumb { +html::-webkit-scrollbar-thumb, div::-webkit-scrollbar-thumb, nav::-webkit-scrollbar-thumb, .chosen-results::-webkit-scrollbar-thumb { background-color: var(--sliderBG); border-radius: 6px; border: 3px solid var(--scrollbarBG); @@ -1122,3 +1117,18 @@ div::-webkit-scrollbar-thumb, nav::-webkit-scrollbar-thumb, .chosen-results::-we width: 100%; } } + +/* +++ This block should always be located at the end! */ +.hidden { + display: none; +} + +.hidden-shift { + position: absolute !important; left: -999em !important; +} + +.invisible { + visibility: hidden; +} +/* --- This block should always be located at the end! */ + diff --git a/web/skins/classic/css/base/views/event.css b/web/skins/classic/css/base/views/event.css index f837092f7c..6b8fb02257 100644 --- a/web/skins/classic/css/base/views/event.css +++ b/web/skins/classic/css/base/views/event.css @@ -359,3 +359,74 @@ svg.zones { float: left; z-index: 1000; } + +button.btn.btn-zoom-out { + padding: 0; + background-color: rgba(150,150,150,0.5); + position: absolute; + left: 2px; + top: 2px; + z-index: 10; + text-shadow: + -1px -1px 0 #575757, + 1px -1px 0 #575757, + -1px 1px 0 #575757, + 1px 1px 0 #575757; +} + +button.btn.btn-zoom-in { + padding: 0; + background-color: rgba(150,150,150,0.5); + position: absolute; + right: 2px; + top: 2px; + z-index: 10; + text-shadow: + -1px -1px 0 #575757, + 1px -1px 0 #575757, + -1px 1px 0 #575757, + 1px 1px 0 #575757; +} + +button.btn.btn-edit-monitor { + padding: 0; + background-color: rgba(150,150,150,0.5); + z-index: 10; + text-shadow: + -1px -1px 0 #575757, + 1px -1px 0 #575757, + -1px 1px 0 #575757, + 1px 1px 0 #575757; +} + +button.btn.btn-view-watch, button.btn.btn-fullscreen, .ratioControl { + display: none; +} + +.block-button-center { + position: absolute; + left: 35%; + right: 35%; + top: 2px; + z-index: 10; +} + +button.btn.btn-zoom-out:focus, +button.btn.btn-zoom-in:focus, +button.btn.btn-view-watch:focus, +button.btn.btn-edit-monitor:focus { + outline: 0; + box-shadow: none; +} + +button.btn.btn-zoom-out:hover, +button.btn.btn-zoom-in:hover, +button.btn.btn-edit-monitor:hover, +button.btn.btn-view-watch:hover { + background-color: darkgrey; +} + +/*Video.js override*/ +.vjs-tech { + pointer-events: auto; +} diff --git a/web/skins/classic/css/base/views/events.css b/web/skins/classic/css/base/views/events.css index ca48dc57b2..1c907f1921 100644 --- a/web/skins/classic/css/base/views/events.css +++ b/web/skins/classic/css/base/views/events.css @@ -77,3 +77,13 @@ body.sticky #eventTable thead { .term-value, .chosen-container{ width: 100% !important; } + +.bs-bars { + width: 100%; +} +#toolbar .row { + width: 100%; +} +#toolbar .row >div { + padding: 0; +} diff --git a/web/skins/classic/css/base/views/frames.css b/web/skins/classic/css/base/views/frames.css index 249fa37518..850df6bf32 100644 --- a/web/skins/classic/css/base/views/frames.css +++ b/web/skins/classic/css/base/views/frames.css @@ -3,7 +3,8 @@ } tr.alarm { - background-color: #fa8072; + background-color: var(--alarmBG); + color: var(--alarmText); } tr.bulk { diff --git a/web/skins/classic/css/base/views/montage.css b/web/skins/classic/css/base/views/montage.css index 5606570c5f..2ffe3c5e98 100644 --- a/web/skins/classic/css/base/views/montage.css +++ b/web/skins/classic/css/base/views/montage.css @@ -14,9 +14,7 @@ overflow: hidden; /* When trying to use the NEW Scale algorithm */ } -#monitors .grid-stack-item .imageFeed , -#monitors .grid-stack-item .imageFeed , -#monitors .grid-stack-item .imageFeed { +.monitor .imageFeed { border: 2px solid #999999; } diff --git a/web/skins/classic/css/dark/skin.css b/web/skins/classic/css/dark/skin.css index ea0ecba67e..5b7084cebf 100644 --- a/web/skins/classic/css/dark/skin.css +++ b/web/skins/classic/css/dark/skin.css @@ -167,7 +167,7 @@ button:disabled, input[disabled], input[type=button]:disabled, input[type=submit]:disabled, -.btn-primary:disabled { +.btn-primary.disabled, .btn-primary:disabled, .btn-secondary.disabled, .btn-secondary:disabled { color: #888888; background-color: #666666; border-color: #666666; @@ -290,6 +290,10 @@ ul.nav.nav-pills.flex-column { background:#444444; } +.monitor .monitorStatus { + background-color: #222222; +} + /* Change scrollbar style */ ::-webkit-scrollbar, div::-webkit-scrollbar, nav::-webkit-scrollbar, .chosen-results::-webkit-scrollbar { width: 11px; diff --git a/web/skins/classic/includes/config.php b/web/skins/classic/includes/config.php index 684b8b51cb..09824a86d5 100644 --- a/web/skins/classic/includes/config.php +++ b/web/skins/classic/includes/config.php @@ -44,18 +44,39 @@ $scales = array( # We use 0 instead of words because we are saving this in the monitor # and use this array to populate the default scale option - '0' => translate('Auto'), + '0' => translate('Auto'), # '400' => '4x', # '300' => '3x', # '200' => '2x', # '150' => '1.5x', - '100' => translate('Actual'), + '100' => translate('Actual'), # '75' => '3/4x', # '50' => '1/2x', # '33' => '1/3x', # '25' => '1/4x', # '12.5' => '1/8x', - 'fit_to_width' => translate('Fit to width'), + 'fit_to_width' => translate('Fit to width'), + '480px' => translate('Max 480px'), + '640px' => translate('Max 640px'), + '800px' => translate('Max 800px'), + '1024px' => translate('Max 1024px'), + '1280px' => translate('Max 1280px'), + '1600px' => translate('Max 1600px'), +); + +$streamQuality = array( + # In % + '+50' => '+50%', + '+40' => '+40%', + '+30' => '+30%', + '+20' => '+20%', + '+10' => '+10%', + '0' => translate('Optimal'), + '-10' => '-10%', + '-20' => '-20%', + '-30' => '-30%', + '-40' => '-40%', + '-50' => '-50%', ); if ( isset($_REQUEST['view']) && ($_REQUEST['view'] == 'montage') ) { diff --git a/web/skins/classic/includes/functions.php b/web/skins/classic/includes/functions.php index c7dd85d3cf..c8056eab55 100644 --- a/web/skins/classic/includes/functions.php +++ b/web/skins/classic/includes/functions.php @@ -91,7 +91,12 @@ function output_cache_busted_stylesheet_links($files) { <?php echo validHtmlStr(ZM_WEB_TITLE_PREFIX) . ' - ' . validHtmlStr($title) ?> + +'; +} else if ( file_exists("skins/$skin/css/$css/graphics/favicon.ico") ) { echo " @@ -992,9 +997,10 @@ function xhtmlFooter() { if ( $basename == 'montage' ) { echo output_script_if_exists(array('assets/gridstack/dist/gridstack-all.js')); echo output_script_if_exists(array('assets/jquery.panzoom/dist/jquery.panzoom.js')); - } - if ( $basename == 'watch' ) { + echo output_script_if_exists(array('js/panzoom.js')); + } else if ( $basename == 'watch' || $basename == 'event') { echo output_script_if_exists(array('assets/jquery.panzoom/dist/jquery.panzoom.js')); + echo output_script_if_exists(array('js/panzoom.js')); } echo output_script_if_exists(array( @@ -1007,6 +1013,7 @@ function xhtmlFooter() { 'js/bootstrap-table-1.22.3/extensions/cookie/bootstrap-table-cookie.js', 'js/bootstrap-table-1.22.3/extensions/toolbar/bootstrap-table-toolbar.min.js', 'js/bootstrap-table-1.22.3/extensions/auto-refresh/bootstrap-table-auto-refresh.min.js', + 'js/bootstrap-table-1.22.3/extensions/mobile/bootstrap-table-mobile.js', 'js/chosen/chosen.jquery.js', 'js/dateTimePicker/jquery-ui-timepicker-addon.js', 'js/Server.js', diff --git a/web/skins/classic/js/skin.js b/web/skins/classic/js/skin.js index e24add616d..ad30a7e943 100644 --- a/web/skins/classic/js/skin.js +++ b/web/skins/classic/js/skin.js @@ -38,6 +38,9 @@ var icons = { detailClose: 'fa-minus' }; +var panZoomEnabled = true; //Add it to settings in the future +var expiredTap; //Time between touch screen clicks. Used to analyze double clicks + function checkSize() { if ( 0 ) { if (window.outerHeight) { @@ -311,22 +314,7 @@ if ( currentView != 'none' && currentView != 'login' ) { const objIconButton = _this_.find("i"); const obj = $j(_this_.attr('data-flip-сontrol-object')); - if ( obj.is(":visible") && !obj.hasClass("hidden-shift")) { - if (objIconButton.is('[class~="material-icons"]')) { // use material-icons - objIconButton.html(objIconButton.attr('data-icon-hidden')); - } else if (objIconButton.is('[class*="fa-"]')) { //use Font Awesome - objIconButton.removeClass(objIconButton.attr('data-icon-visible')).addClass(objIconButton.attr('data-icon-hidden')); - } - setCookie('zmFilterBarFlip'+_this_.attr('data-flip-сontrol-object'), 'hidden'); - } else { //hidden - obj.removeClass('hidden-shift').addClass('hidden'); //It is necessary to make the block invisible both for JS and for humans - if (objIconButton.is('[class~="material-icons"]')) { // use material-icons - objIconButton.html(objIconButton.attr('data-icon-visible')); - } else if (objIconButton.is('[class*="fa-"]')) { //use Font Awesome - objIconButton.removeClass(objIconButton.attr('data-icon-hidden')).addClass(objIconButton.attr('data-icon-visible')); - } - setCookie('zmFilterBarFlip'+_this_.attr('data-flip-сontrol-object'), 'visible'); - } + changeButtonIcon(_this_, objIconButton); const nameFuncBefore = _this_.attr('data-flip-сontrol-run-before-func') ? _this_.attr('data-flip-сontrol-run-before-func') : null; const nameFuncAfter = _this_.attr('data-flip-сontrol-run-after-func') ? _this_.attr('data-flip-сontrol-run-after-func') : null; @@ -337,13 +325,15 @@ if ( currentView != 'none' && currentView != 'login' ) { if (typeof safeFunc[nameFunc] === 'function') safeFunc[nameFunc](); }); } - obj.slideToggle("fast", function() { - if (nameFuncAfterComplet) { - $j.each(nameFuncAfterComplet.split(' '), function(i, nameFunc) { - if (typeof safeFunc[nameFunc] === 'function') safeFunc[nameFunc](); - }); - } - }); + if (!_this_.attr('data-on-click-true')) { + obj.slideToggle("fast", function() { + if (nameFuncAfterComplet) { + $j.each(nameFuncAfterComplet.split(' '), function(i, nameFunc) { + if (typeof safeFunc[nameFunc] === 'function') safeFunc[nameFunc](); + }); + } + }); + } if (nameFuncAfter) { $j.each(nameFuncAfter.split(' '), function(i, nameFunc) { if (typeof safeFunc[nameFunc] === 'function') safeFunc[nameFunc](); @@ -352,9 +342,10 @@ if ( currentView != 'none' && currentView != 'login' ) { }); // Manage visible filter bar & control button (after document ready) - $j("[data-flip-сontrol-object]").each(function() { //let's go through all objects and set icons + $j("[data-flip-сontrol-object]").each(function() { //let's go through all objects (buttons) and set icons const _this_ = $j(this); const сookie = getCookie('zmFilterBarFlip'+_this_.attr('data-flip-сontrol-object')); + const initialStateIcon = _this_.attr('data-initial-state-icon'); //"visible"=Opened block , "hidden"=Closed block or "undefined"=use cookie const objIconButton = _this_.find("i"); const obj = $j(_this_.attr('data-flip-сontrol-object')); @@ -362,20 +353,24 @@ if ( currentView != 'none' && currentView != 'login' ) { obj.wrap('
'); } - if (сookie == 'hidden') { - if (objIconButton.is('[class~="material-icons"]')) { // use material-icons + // initialStateIcon takes priority. If there is no cookie, we assume that it is 'visible' + const stateIcon = (initialStateIcon) ? initialStateIcon : ((сookie == 'hidden') ? 'hidden' : 'visible'); + if (objIconButton.is('[class~="material-icons"]')) { // use material-icons + if (stateIcon == 'hidden') { objIconButton.html(objIconButton.attr('data-icon-hidden')); - } else if (objIconButton.is('[class*="fa-"]')) { //use Font Awesome - objIconButton.addClass(objIconButton.attr('data-icon-hidden')); - } - obj.addClass('hidden-shift'); //To prevent jerking when running the "Chosen" script, it is necessary to make the block visible to JS, but invisible to humans! - } else { //no cookies or opened. - if (objIconButton.is('[class~="material-icons"]')) { // use material-icons + obj.addClass('hidden-shift'); //To prevent jerking when running the "Chosen" script, it is necessary to make the block visible to JS, but invisible to humans! + } else { objIconButton.html(objIconButton.attr('data-icon-visible')); - } else if (objIconButton.is('[class*="fa-"]')) { //use Font Awesome + obj.removeClass('hidden-shift'); + } + } else if (objIconButton.is('[class*="fa-"]')) { //use Font Awesome + if (stateIcon == 'hidden') { + objIconButton.addClass(objIconButton.attr('data-icon-hidden')); + obj.addClass('hidden-shift'); //To prevent jerking when running the "Chosen" script, it is necessary to make the block visible to JS, but invisible to humans! + } else { objIconButton.addClass(objIconButton.attr('data-icon-visible')); + obj.removeClass('hidden-shift'); } - obj.removeClass('hidden-shift'); } }); @@ -415,6 +410,31 @@ if ( currentView != 'none' && currentView != 'login' ) { applyChosen(); }); + /* + * params{visibility: null "visible" or "hidden"} - state of the panel before pressing button + */ + function changeButtonIcon(pressedBtn, target, params) { + const visibility = (!params) ? null : params.visibility; + const objIconButton = pressedBtn.find("i"); + const obj = $j(pressedBtn.attr('data-flip-сontrol-object')); + if ((visibility == "visible") || (obj.is(":visible") && !obj.hasClass("hidden-shift"))) { + if (objIconButton.is('[class~="material-icons"]')) { // use material-icons + objIconButton.html(objIconButton.attr('data-icon-hidden')); + } else if (objIconButton.is('[class*="fa-"]')) { //use Font Awesome + objIconButton.removeClass(objIconButton.attr('data-icon-visible')).addClass(objIconButton.attr('data-icon-hidden')); + } + setCookie('zmFilterBarFlip'+pressedBtn.attr('data-flip-сontrol-object'), 'hidden'); + } else { //hidden + obj.removeClass('hidden-shift').addClass('hidden'); //It is necessary to make the block invisible both for JS and for humans + if (objIconButton.is('[class~="material-icons"]')) { // use material-icons + objIconButton.html(objIconButton.attr('data-icon-visible')); + } else if (objIconButton.is('[class*="fa-"]')) { //use Font Awesome + objIconButton.removeClass(objIconButton.attr('data-icon-hidden')).addClass(objIconButton.attr('data-icon-visible')); + } + setCookie('zmFilterBarFlip'+pressedBtn.attr('data-flip-сontrol-object'), 'visible'); + } + } + // After retieving modal html via Ajax, this will insert it into the DOM function insertModalHtml(name, html) { let modal = $j('#' + name); @@ -472,14 +492,14 @@ if ( currentView != 'none' && currentView != 'login' ) { // Update authentication token. auth_hash = data.auth; } + delete data.auth; } if (data.auth_relay) { auth_relay = data.auth_relay; + delete data.auth_relay; } // iterate through all the keys then update each element id with the same name - for (var key of Object.keys(data)) { - if ( key == "auth" ) continue; - if ( key == "auth_relay" ) continue; + for (const key of Object.keys(data)) { if ( $j('#'+key).hasClass("show") ) continue; // don't update if the user has the dropdown open if ( $j('#'+key).length ) $j('#'+key).replaceWith(data[key]); if ( key == 'getBandwidthHTML' ) bwClickFunction(); @@ -677,40 +697,36 @@ function endOfResize(e) { * figures out where bottomEl is in the viewport * does calculations * */ -function scaleToFit(baseWidth, baseHeight, scaleEl, bottomEl, container) { +function scaleToFit(baseWidth, baseHeight, scaleEl, bottomEl, container, panZoomScale = 1) { $j(window).on('resize', endOfResize); //set delayed scaling when Scale to Fit is selected - const ratio = baseWidth / baseHeight; if (!container) container = $j('#content'); if (!container) { console.error("No container found"); return; } - if (!bottomEl || !bottomEl.length) { - bottomEl = $j(container[0].lastElementChild); - } + const ratio = baseWidth / baseHeight; const viewPort = $j(window); // jquery does not provide a bottom offset, and offset does not include margins. outerHeight true minus false gives total vertical margins. - const bottomLoc = bottomEl.offset().top + (bottomEl.outerHeight(true) - bottomEl.outerHeight()) + bottomEl.outerHeight(true); - console.log("bottomLoc: " + bottomEl.offset().top + " + (" + bottomEl.outerHeight(true) + ' - ' + bottomEl.outerHeight() +') + '+bottomEl.outerHeight(true) + '='+bottomLoc); + let bottomLoc = 0; + if (bottomEl !== false) { + if (!bottomEl || !bottomEl.length) { + bottomEl = $j(container[0].lastElementChild); + } + bottomLoc = bottomEl.offset().top + (bottomEl.outerHeight(true) - bottomEl.outerHeight()) + bottomEl.outerHeight(true); + console.log("bottomLoc: " + bottomEl.offset().top + " + (" + bottomEl.outerHeight(true) + ' - ' + bottomEl.outerHeight() +') + '+bottomEl.outerHeight(true) + '='+bottomLoc); + } let newHeight = viewPort.height() - (bottomLoc - scaleEl.outerHeight(true)); - console.log("newHeight = " + viewPort.height() +" - " + bottomLoc + ' - ' + scaleEl.outerHeight(true)+'='+newHeight); let newWidth = ratio * newHeight; + console.log('new ', newWidth, newHeight, 'viewport', viewPort.height(), scaleEl); - // Let's recalculate everything and reduce the height a little. Necessary if "padding" is specified for "wrapperEventVideo" - padding = parseInt(container.css("padding-left")) + parseInt(container.css("padding-right")); - newWidth -= padding; - newHeight = newWidth / ratio; - - console.log("newWidth = ", newWidth, "container width:", container.innerWidth()-padding); - - if (newHeight < 0 || newWidth > container.innerWidth()-padding) { + if (newHeight < 0 || newWidth > container.width()) { // Doesn't fit on screen anyways? - newWidth = container.innerWidth()-padding; + newWidth = container.width(); newHeight = newWidth / ratio; + console.log('new ', newWidth, newHeight, ratio); } - console.log("newWidth = " + newWidth); - let autoScale = Math.round(newWidth / baseWidth * SCALE_BASE); + let autoScale = Math.round(newWidth / baseWidth * SCALE_BASE * panZoomScale); /* IgorA100 not required due to new "Scale" algorithm & new PanZoom (may 2024) const scales = $j('#scale option').map(function() { return parseInt($j(this).val()); @@ -727,7 +743,10 @@ function scaleToFit(baseWidth, baseHeight, scaleEl, bottomEl, container) { autoScale = closest; } */ + // Floor to nearest value % 5. THe 5 is somewhat arbitrary. The point is that scaling by 88% is not better than 85%. Perhaps it should be to the nearest 10. Or 25 even. + autoScale = 5 * Math.floor(autoScale / 5); if (autoScale < 10) autoScale = 10; + console.log(`container.height=${container.height()}, newWidth=${newWidth}, newHeight=${newHeight}, container width=${container.width()}, autoScale=${autoScale}`); return {width: Math.floor(newWidth), height: Math.floor(newHeight), autoScale: autoScale}; } @@ -745,7 +764,19 @@ function setButtonState(element_id, btnClass) { } } +function isJSON(str) { + if (typeof str !== 'string') return false; + try { + const result = JSON.parse(str); + const type = Object.prototype.toString.call(result); + return type === '[object Object]' || type === '[object Array]'; // We only pass objects and arrays + } catch (e) { + return false; // This is also not JSON + } +}; + function setCookie(name, value, seconds) { + var newValue = (typeof value === 'string' || typeof value === 'boolean') ? value : JSON.stringify(value); let expires = ""; if (seconds) { const date = new Date(); @@ -755,18 +786,28 @@ function setCookie(name, value, seconds) { // 2147483647 is 2^31 - 1 which is January of 2038 to avoid the 32bit integer overflow bug. expires = "; max-age=2147483647"; } - document.cookie = name + "=" + (value || "") + expires + "; path=/; samesite=strict"; + document.cookie = name + "=" + (newValue || "") + expires + "; path=/; samesite=strict"; } +/* +* If JSON is stored in cookies, the function will return an array or object of values. +*/ function getCookie(name) { var nameEQ = name + "="; + var result = null; var ca = document.cookie.split(';'); for (var i=0; i < ca.length; i++) { + if (result) break; var c = ca[i]; while (c.charAt(0)==' ') c = c.substring(1, c.length); - if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); + if (c.indexOf(nameEQ) == 0) { + result = c.substring(nameEQ.length, c.length); + break; + } } - return null; + if (isJSON(result)) result = JSON.parse(result); + + return result; } function delCookie(name) { @@ -905,8 +946,7 @@ function stateStuff(action, runState, newState) { function logAjaxFail(jqxhr, textStatus, error) { console.log("Request Failed: " + textStatus + ", " + error); if ( ! jqxhr.responseText ) { - console.log("Ajax request failed. No responseText. jqxhr follows:"); - console.log(jqxhr); + console.log("Ajax request failed. No responseText. jqxhr follows:\n", jqxhr); return; } var responseText = jqxhr.responseText.replace(/(<([^>]+)>)/gi, '').trim(); // strip any html or whitespace from the response @@ -1151,9 +1191,65 @@ function applyChosen() { $j('.chosen.chosen-auto-width').chosen({allow_single_deselect: true, disable_search_threshold: limit_search_threshold, search_contains: true, width: "auto"}); } -const font = new FontFaceObserver('Material Icons', {weight: 400}); -font.load().then(function() { - $j('.material-icons').css('display', 'inline-block'); -}, function() { - $j('.material-icons').css('display', 'inline-block'); -}); +function stringToNumber(str) { + return parseInt(str.replace(/\D/g, '')); +} + +function loadFontFaceObserver() { + const font = new FontFaceObserver('Material Icons', {weight: 400}); + font.load().then(function() { + $j('.material-icons').css('display', 'inline-block'); + }, function() { + $j('.material-icons').css('display', 'inline-block'); + }); +} + +var doubleClickOnStream = function(event, touchEvent) { + let target = null; + if (event.target) {// Click NOT on touch screen, use THIS + //Process only double clicks directly on the image, excluding clicks, + //for example, on zoom buttons and other elements located in the image area. + if (event.target.id && + (event.target.id.indexOf('evtStream') != -1 || event.target.id.indexOf('liveStream') != -1)) { + target = this; + } + } else {// Click on touch screen, use EVENT + if (touchEvent.target.id && + (touchEvent.target.id.indexOf('evtStream') != -1 || touchEvent.target.id.indexOf('liveStream') != -1)) { + target = event; + } + } + + if (target) { + if (document.fullscreenElement) { + closeFullscreen(); + } else { + openFullscreen(target); + } + if (isMobile()) { + setTimeout(function() { + //For some mobile devices resizing does not work. You need to set a delay and re-call the 'resize' event + window.dispatchEvent(new Event('resize')); + }, 500); + } + } +}; + +var doubleTouch = function(e) { + if (e.touches.length === 1) { + if (!expiredTap) { + expiredTap = e.timeStamp + 300; + } else if (e.timeStamp <= expiredTap) { + // remove the default of this event ( Zoom ) + e.preventDefault(); + doubleClickOnStream(this, e); + // then reset the variable for other "double Touches" event + expiredTap = null; + } else { + // if the second touch was expired, make it as it's the first + expiredTap = e.timeStamp + 300; + } + } +}; + +loadFontFaceObserver(); diff --git a/web/skins/classic/js/skin.js.php b/web/skins/classic/js/skin.js.php index 0cb3814b07..a1db832b38 100644 --- a/web/skins/classic/js/skin.js.php +++ b/web/skins/classic/js/skin.js.php @@ -138,7 +138,6 @@ stateStrings[STATE_PREALARM] = ""; stateStrings[STATE_ALARM] = ""; stateStrings[STATE_ALERT] = ""; -stateStrings[STATE_TAPE] = ""; '; + $html .= ''; + $html .= htmlSelect($name.'[]', $options, + (isset($_SESSION[$name])?$_SESSION[$name]:''), + array( + 'data-on-change'=>'submitThisForm', + 'class'=>'chosen', + 'multiple'=>'multiple', + 'data-placeholder'=>'All', + ) + ); + $html .= ''; + $html .= ''.PHP_EOL; + return $html; +} + +function buildMonitorsFilters() { + global $user, $Servers; + require_once('includes/Monitor.php'); + + zm_session_start(); + foreach (array('GroupId','Capturing','Analysing','Recording','ServerId','StorageId','Status','MonitorId','MonitorName','Source') as $var) { + if (isset($_REQUEST[$var])) { + if ($_REQUEST[$var] != '') { + $_SESSION[$var] = $_REQUEST[$var]; + } else { + unset($_SESSION[$var]); + } + } else if (isset($_REQUEST['filtering'])) { unset($_SESSION[$var]); } - } else if (isset($_REQUEST['filtering'])) { - unset($_SESSION[$var]); } -} -session_write_close(); + session_write_close(); -$storage_areas = ZM\Storage::find(); -$StorageById = array(); -foreach ($storage_areas as $S) { - $StorageById[$S->Id()] = $S; -} + $storage_areas = ZM\Storage::find(); + $StorageById = array(); + foreach ($storage_areas as $S) { + $StorageById[$S->Id()] = $S; + } -$ServersById = array(); -foreach ($Servers as $s) { - $ServersById[$s->Id()] = $s; -} + $ServersById = array(); + foreach ($Servers as $s) { + $ServersById[$s->Id()] = $s; + } -$html = + $html = '
@@ -53,63 +72,67 @@ '; -$groupSql = ''; -if (canView('Groups')) { - $GroupsById = array(); - foreach (ZM\Group::find() as $G) { - $GroupsById[$G->Id()] = $G; + $groupSql = ''; + if (canView('Groups')) { + $GroupsById = array(); + foreach (ZM\Group::find() as $G) { + $GroupsById[$G->Id()] = $G; + } + + if (count($GroupsById)) { + $html .= ''; + $html .= ''; + # This will end up with the group_id of the deepest selection + $group_id = isset($_SESSION['GroupId']) ? $_SESSION['GroupId'] : null; + $html .= ZM\Group::get_group_dropdown(); + $groupSql = ZM\Group::get_group_sql($group_id); + $html .= ''; + $html .= ''; + } } - if (count($GroupsById)) { - $html .= ''; - $html .= ''; - # This will end up with the group_id of the deepest selection - $group_id = isset($_SESSION['GroupId']) ? $_SESSION['GroupId'] : null; - $html .= ZM\Group::get_group_dropdown(); - $groupSql = ZM\Group::get_group_sql($group_id); - $html .= ''; - $html .= ''; + $selected_monitor_ids = isset($_SESSION['MonitorId']) ? $_SESSION['MonitorId'] : array(); + if ( !is_array($selected_monitor_ids) ) { + $selected_monitor_ids = array($selected_monitor_ids); } -} -$selected_monitor_ids = isset($_SESSION['MonitorId']) ? $_SESSION['MonitorId'] : array(); -if ( !is_array($selected_monitor_ids) ) { - $selected_monitor_ids = array($selected_monitor_ids); -} + $conditions = array(); + $values = array(); -$conditions = array(); -$values = array(); - -if ( $groupSql ) - $conditions[] = $groupSql; -foreach ( array('ServerId','StorageId','Status','Capturing','Analysing','Recording') as $filter ) { - if ( isset($_SESSION[$filter]) ) { - if ( is_array($_SESSION[$filter]) ) { - $conditions[] = '`'.$filter . '` IN ('.implode(',', array_map(function(){return '?';}, $_SESSION[$filter])). ')'; - $values = array_merge($values, $_SESSION[$filter]); - } else { - $conditions[] = '`'.$filter . '`=?'; - $values[] = $_SESSION[$filter]; + if ( $groupSql ) + $conditions[] = $groupSql; + foreach ( array('ServerId','StorageId','Status','Capturing','Analysing','Recording') as $filter ) { + if ( isset($_SESSION[$filter]) ) { + if ( is_array($_SESSION[$filter]) ) { + $conditions[] = '`'.$filter . '` IN ('.implode(',', array_map(function(){return '?';}, $_SESSION[$filter])). ')'; + $values = array_merge($values, $_SESSION[$filter]); + } else { + $conditions[] = '`'.$filter . '`=?'; + $values[] = $_SESSION[$filter]; + } } + } # end foreach filter + + if (count($user->unviewableMonitorIds()) ) { + $ids = $user->viewableMonitorIds(); + $conditions[] = 'M.Id IN ('.implode(',',array_map(function(){return '?';}, $ids)).')'; + $values = array_merge($values, $ids); } -} # end foreach filter -if (count($user->unviewableMonitorIds()) ) { - $ids = $user->viewableMonitorIds(); - $conditions[] = 'M.Id IN ('.implode(',',array_map(function(){return '?';}, $ids)).')'; - $values = array_merge($values, $ids); -} + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''.PHP_EOL; -$html .= ''; -$html .= ''; -$html .= ''; -$html .= ''.PHP_EOL; + $html .= addFilterSelect('Capturing', array('None'=>translate('None'), 'Always'=>translate('Always'), 'OnDemand'=>translate('On Demand'))); + $html .= addFilterSelect('Analysing', array('None'=>translate('None'), 'Always'=>translate('Always'))); + $html .= addFilterSelect('Recording', array('None'=>translate('None'), 'OnMotion'=>translate('On Motion'),'Always'=>translate('Always'))); -function addFilterSelect($name, $options) { - $html = ''; - $html .= ''; - $html .= htmlSelect($name.'[]', $options, - (isset($_SESSION[$name])?$_SESSION[$name]:''), + if ( count($ServersById) > 1 ) { + $html .= ''; + $html .= ''; + $html .= htmlSelect('ServerId[]', $ServersById, + (isset($_SESSION['ServerId'])?$_SESSION['ServerId']:''), array( 'data-on-change'=>'submitThisForm', 'class'=>'chosen', @@ -117,64 +140,43 @@ function addFilterSelect($name, $options) { 'data-placeholder'=>'All', ) ); - $html .= ''; - $html .= ''.PHP_EOL; - return $html; -} - -$html .= addFilterSelect('Capturing', array('None'=>translate('None'), 'Always'=>translate('Always'), 'OnDemand'=>translate('On Demand'))); -$html .= addFilterSelect('Analysing', array('None'=>translate('None'), 'Always'=>translate('Always'))); -$html .= addFilterSelect('Recording', array('None'=>translate('None'), 'OnMotion'=>translate('On Motion'),'Always'=>translate('Always'))); + $html .= ''; + $html .= ''; + } # end if have Servers -if ( count($ServersById) > 1 ) { - $html .= ''; - $html .= ''; - $html .= htmlSelect('ServerId[]', $ServersById, - (isset($_SESSION['ServerId'])?$_SESSION['ServerId']:''), - array( - 'data-on-change'=>'submitThisForm', - 'class'=>'chosen', - 'multiple'=>'multiple', - 'data-placeholder'=>'All', - ) - ); - $html .= ''; - $html .= ''; -} # end if have Servers + if ( count($StorageById) > 1 ) { + $html .= ''; + $html .= ''; + $html .= htmlSelect('StorageId[]', $StorageById, + (isset($_SESSION['StorageId'])?$_SESSION['StorageId']:''), + array( + 'data-on-change'=>'submitThisForm', + 'class'=>'chosen', + 'multiple'=>'multiple', + 'data-placeholder'=>'All', + ) ); + $html .= ''; + $html .= ''; + } # end if have Storage Areas -if ( count($StorageById) > 1 ) { - $html .= ''; + $html .= ''; + $status_options = array( + 'Unknown' => translate('StatusUnknown'), + 'NotRunning' => translate('StatusNotRunning'), + 'Running' => translate('StatusRunning'), + 'Connected' => translate('StatusConnected'), + ); $html .= ''; - $html .= htmlSelect('StorageId[]', $StorageById, - (isset($_SESSION['StorageId'])?$_SESSION['StorageId']:''), + $html .= htmlSelect( 'Status[]', $status_options, + ( isset($_SESSION['Status']) ? $_SESSION['Status'] : '' ), array( 'data-on-change'=>'submitThisForm', 'class'=>'chosen', 'multiple'=>'multiple', - 'data-placeholder'=>'All', + 'data-placeholder'=>'All' ) ); $html .= ''; $html .= ''; -} # end if have Storage Areas - -$html .= ''; -$status_options = array( - 'Unknown' => translate('StatusUnknown'), - 'NotRunning' => translate('StatusNotRunning'), - 'Running' => translate('StatusRunning'), - 'Connected' => translate('StatusConnected'), - ); - $html .= ''; - $html .= htmlSelect( 'Status[]', $status_options, - ( isset($_SESSION['Status']) ? $_SESSION['Status'] : '' ), - array( - 'data-on-change'=>'submitThisForm', - 'class'=>'chosen', - 'multiple'=>'multiple', - 'data-placeholder'=>'All' - ) ); - $html .= ''; - $html .= ''; $html .= ''; $html .= ''; @@ -183,10 +185,10 @@ function addFilterSelect($name, $options) { $html .= ''; $sqlAll = 'SELECT M.*, S.*, E.* - FROM Monitors AS M - LEFT JOIN Monitor_Status AS S ON S.MonitorId=M.Id - LEFT JOIN Event_Summaries AS E ON E.MonitorId=M.Id - WHERE M.`Deleted`=false'; + FROM Monitors AS M + LEFT JOIN Monitor_Status AS S ON S.MonitorId=M.Id + LEFT JOIN Event_Summaries AS E ON E.MonitorId=M.Id + WHERE M.`Deleted`=false'; $sqlSelected = $sqlAll . ( count($conditions) ? ' AND ' . implode(' AND ', $conditions) : '' ).' ORDER BY Sequence ASC'; $monitors = dbFetchAll($sqlSelected, null, $values); @@ -195,7 +197,7 @@ function addFilterSelect($name, $options) { if ( visibleMonitor($row['Id']) ) { #We count only available monitors. ++$colAllAvailableMonitors; } - } + } $displayMonitors = array(); $monitors_dropdown = array(); @@ -283,6 +285,15 @@ function addFilterSelect($name, $options) { $display_monitor_ids = array_map(function($monitor_row){return $monitor_row['Id'];}, $displayMonitors); $html .= ''; $html .= ''; - echo $html; + $html .= '
'; + + return [ + "filterBar" => $html, + "displayMonitors" => $displayMonitors, + "storage_areas" => $storage_areas, //Console page + "StorageById" => $StorageById, //Console page + "colAllAvailableMonitors" => $colAllAvailableMonitors, //Console page + "selected_monitor_ids" => $selected_monitor_ids + ]; +} ?> -
diff --git a/web/skins/classic/views/console.php b/web/skins/classic/views/console.php index d231228c44..2718cc5d58 100644 --- a/web/skins/classic/views/console.php +++ b/web/skins/classic/views/console.php @@ -97,10 +97,13 @@ require_once('includes/Group_Monitor.php'); $navbar = getNavBarHTML(); -ob_start(); include('_monitor_filters.php'); -$filterbar = ob_get_contents(); -ob_end_clean(); +$resultMonitorFilters = buildMonitorsFilters(); +$filterbar = $resultMonitorFilters['filterBar']; +$displayMonitors = $resultMonitorFilters['displayMonitors']; +$storage_areas = $resultMonitorFilters['storage_areas']; +$StorageById = $resultMonitorFilters['StorageById']; +$colAllAvailableMonitors = $resultMonitorFilters['selected_monitor_ids']; $displayMonitorIds = array_map(function($m){return $m['Id'];}, $displayMonitors); diff --git a/web/skins/classic/views/cycle.php b/web/skins/classic/views/cycle.php index a2c44a836d..fc906bb740 100644 --- a/web/skins/classic/views/cycle.php +++ b/web/skins/classic/views/cycle.php @@ -23,10 +23,10 @@ return; } -ob_start(); include('_monitor_filters.php'); -$filterbar = ob_get_contents(); -ob_end_clean(); +$resultMonitorFilters = buildMonitorsFilters(); +$filterbar = $resultMonitorFilters['filterBar']; +$displayMonitors = $resultMonitorFilters['displayMonitors']; $options = array(); diff --git a/web/skins/classic/views/event.php b/web/skins/classic/views/event.php index 6b61398352..ebe8a72742 100644 --- a/web/skins/classic/views/event.php +++ b/web/skins/classic/views/event.php @@ -28,8 +28,8 @@ require_once('includes/Filter.php'); require_once('includes/Zone.php'); -$eid = validInt($_REQUEST['eid']); -$fid = !empty($_REQUEST['fid']) ? validInt($_REQUEST['fid']) : 1; +$eid = validCardinal($_REQUEST['eid']); +$fid = !empty($_REQUEST['fid']) ? validCardinal($_REQUEST['fid']) : 1; $Event = new ZM\Event($eid); $monitor = $Event->Monitor(); @@ -52,15 +52,29 @@ zm_setcookie('zmEventRate', $rate); } +// $scaleSelected - temporarily to adapt the new algorithm, because $scale can only be a numeric value! if (isset($_REQUEST['scale'])) { $scale = validInt($_REQUEST['scale']); + $scaleSelected = $_REQUEST['scale']; } else if (isset($_COOKIE['zmEventScale'.$Event->MonitorId()])) { $scale = validInt($_COOKIE['zmEventScale'.$Event->MonitorId()]); + $scaleSelected = $_COOKIE['zmEventScale'.$Event->MonitorId()]; } else { $scale = validInt($monitor->DefaultScale()); + $scaleSelected = $monitor->DefaultScale(); } -if (!validInt($scale) and $scale != '0') { - $scale = '0'; +//if (!validInt($scale) and $scale != '0') { +// $scale = '0'; +//} +$scale = '10'; // temporarily to adapt the new algorithm, not used in new calculations! + +$streamQualitySelected = '0'; +if (isset($_REQUEST['streamQuality'])) { + $streamQualitySelected = $_REQUEST['streamQuality']; +} else if (isset($_COOKIE['zmStreamQuality'])) { + $streamQualitySelected = $_COOKIE['zmStreamQuality']; +} else if (isset($_SESSION['zmStreamQuality']) ) { + $streamQualitySelected = $_SESSION['zmStreamQuality']; } $showZones = false; @@ -223,13 +237,13 @@ - grid_view + grid_view
@@ -242,7 +256,11 @@
- 'changeScale','id'=>'scale')); ?> + 'changeScale','id'=>'scale')); ?> +
+
+ + 'changeStreamQuality','id'=>'streamQuality')); ?>
@@ -319,40 +337,37 @@
+ +
+
-
- -
-
-
-
- -
+ -
-getStreamSrc(array('mode'=>'mpeg', 'scale'=>($scale > 0 ? $scale : 100), 'rate'=>$rate, 'bitrate'=>ZM_WEB_VIDEO_BITRATE, 'maxfps'=>ZM_WEB_VIDEO_MAXFPS, 'format'=>ZM_MPEG_REPLAY_FORMAT, 'replay'=>$replayMode),'&'); - outputVideoStream('evtStream', $streamSrc, reScale( $Event->Width(), $scale ).'px', reScale( $Event->Height(), $scale ).'px', ZM_MPEG_LIVE_FORMAT ); -} else { $streamSrc = $Event->getStreamSrc(array('mode'=>'jpeg', 'frame'=>$fid, 'scale'=>($scale > 0 ? $scale : 100), 'rate'=>$rate, 'maxfps'=>ZM_WEB_VIDEO_MAXFPS, 'replay'=>$replayMode),'&'); if (!canStreamNative()) { echo '
We have detected an inability to stream natively. Unfortunately we no longer support really ancient browsers. Trying anyways.
'; @@ -361,28 +376,19 @@ function($r){return $r >= 0 ? true : false;} ($scale ? reScale($Event->Width(), $scale).'px' : '100%'), ($scale ? reScale($Event->Height(), $scale).'px' : 'auto'), validHtmlStr($Event->Name())); -} // end if stream method +} else if ($player == 'h265web.js') { ?> +
+ +
+
- -
-
-
-
-
-
- -
-
- $monitor->Id()), array('order'=>'Area DESC')) as $zone) { @@ -395,38 +401,38 @@ function($r){return $r >= 0 ? true : false;}
Name() . " (". translate('ID'). "=" . $monitor->Id() . ")"; ?>
-
- - - - - - - - - - - +

+ + + + + + + + + + +

@@ -440,10 +446,10 @@ function($r){return $r >= 0 ? true : false;}
-
-
- : Replay - : +

+
+ : Replay + : 'rateValue')); diff --git a/web/skins/classic/views/events.php b/web/skins/classic/views/events.php index b0a5dc7ee4..c2f7a84e7b 100644 --- a/web/skins/classic/views/events.php +++ b/web/skins/classic/views/events.php @@ -64,11 +64,20 @@ 'val' => $num_terms ? '' : (isset($_COOKIE['eventsTags']) ? $_COOKIE['eventsTags'] : ''), 'cnj' => 'and', 'cookie'=>'eventsTags')); } - $filter->sort_terms(['Group','Monitor','StartDateTime','EndDateTime','Notes','Tags']); + if (!$filter->has_term('Archived')) { + $filter->addTerm(array('attr' => 'Archived', 'op' => '=', + 'val' => $num_terms ? '' : (isset($_COOKIE['zmFilterArchived']) ? $_COOKIE['zmFilterArchived'] : ''), + 'cnj' => 'and', 'cookie'=>'zmFilterArchived')); + } + $filter->sort_terms(['Group','Monitor','StartDateTime','EndDateTime','Notes','Tags','Archived']); #$filter->addTerm(array('cnj'=>'and', 'attr'=>'AlarmFrames', 'op'=> '>', 'val'=>'10')); #$filter->addTerm(array('cnj'=>'and', 'attr'=>'StartDateTime', 'op'=> '<=', 'val'=>'')); } +if (!isset($_COOKIES['zmEventsTable.bs.table.pageList'])) { + zm_setcookie('zmEventsTable.bs.table.pageList', ZM_WEB_EVENTS_PER_PAGE); +} + parseSort(); $filterQuery = $filter->querystring(); @@ -80,7 +89,7 @@
-
+
@@ -88,6 +97,7 @@ +
@@ -98,6 +108,7 @@ echo $filter->widget(); } ?> +
@@ -164,6 +175,9 @@ class="table-sm table-borderless table" + + + diff --git a/web/skins/classic/views/files.php b/web/skins/classic/views/files.php index 0196d4dcd2..f0727db4d8 100644 --- a/web/skins/classic/views/files.php +++ b/web/skins/classic/views/files.php @@ -24,6 +24,15 @@ } $path = (!empty($_REQUEST['path'])) ? $_REQUEST['path'] : ZM_DIR_EVENTS; +$is_ok_path = false; +foreach (ZM\Storage::find() as $storage) { + $rc = strstr($path, $storage->Path(), true); + ZM\Debug("rc from strstr ($rc) $path ".$storage->Path()); + if ((false !== $rc) and ($rc == '')) { + # Must be at the beginning + $is_ok_path = true; + } +} $path_parts = pathinfo($path); if (@is_file($path)) { @@ -54,6 +63,9 @@ function guess_material_icon($file) {
Path is not valid. Path must be below a designated Storage area.
'; +} else { $exploded = explode('/', $path); ZM\Debug(print_r($exploded, true)); $array = array(); @@ -123,6 +135,9 @@ function guess_material_icon($file) { ?> +
cueFrames[cueFrames.length - 1].Delta)) ? eventData.Length : cueFrames[cueFrames.length - 1].Delta; + const event_length = ((!cueFrames.length) || (parseFloat(eventData.Length) > parseFloat(cueFrames[cueFrames.length - 1].Delta))) ? eventData.Length : cueFrames[cueFrames.length - 1].Delta; const span_count = 10; const span_seconds = parseFloat(event_length / span_count); const span_width = parseFloat(containerEl.width() / span_count); @@ -223,6 +210,7 @@ function renderAlarmCues(containerEl) { for (let i=0; i < span_count; i += 1) { html += ''+date.toLocaleTimeString()+''; date.setTime(date.getTime() + span_seconds*1000); + console.log(date); } if (!(cueFrames && cueFrames.length)) { @@ -310,7 +298,83 @@ function changeCodec() { location.replace(thisUrl + '?view=event&eid=' + eventData.Id + filterQuery + sortQuery+'&codec='+$j('#codec').val()); } +function deltaScale() { + return parseInt(currentScale/100*$j('#streamQuality').val()); // "-" - Decrease quality, "+" - Increase image quality in % +} + function changeScale() { + const scaleSel = $j('#scale').val(); + const eventViewer = $j((playerType == 'h265web.js' || playerType == 'video.js') ? '#videoobj' : '#evtStream'); + + const alarmCue = $j('#alarmCues'); + const bottomEl = $j('#replayStatus'); + const landscape = eventData.width / eventData.height > 1 ? true : false; //Image orientation. + + setCookie('zmEventScale'+eventData.MonitorId, scaleSel); + + /*!!! eventData.Width & eventData.Height may differ from the actual size of the broadcast frame due to the "Capture Resolution (pixels)" setting on the source page of the monitor settings !!!*/ + /*!!! Because of this, the Scale is not correct. For example, when recording in 4k, Capture Resolution = FHD, the image with a width of 600px looks terrible! */ + + let newSize; + if (scaleSel == '100') { + //Actual, 100% of original size + newWidth = eventData.Width; + newHeight = eventData.Height; + currentScale = 100; + } else if (scaleSel == '0') { + //Auto, Width is calculated based on the occupied height so that the image and control buttons occupy the visible part of the screen. + newSize = scaleToFit(eventData.Width, eventData.Height, eventViewer, bottomEl, $j('#wrapperEventVideo')); + newWidth = newSize.width; + newHeight = newSize.height; + currentScale = newSize.autoScale; + } else if (scaleSel == 'fit_to_width') { + //Fit to screen width + newSize = scaleToFit(eventData.Width, eventData.Height, eventViewer, false, $j('#wrapperEventVideo')); + newWidth = newSize.width; + newHeight = newSize.height; + currentScale = newSize.autoScale; + } else if (scaleSel.indexOf("px") > -1) { + newSize = scaleToFit(eventData.Width, eventData.Height, eventViewer, false, $j('#wrapperEventVideo')); // Only for calculating the maximum width! + let w = h = ''; + if (landscape) { + w = Math.min(stringToNumber(scaleSel), newSize.width); + h = w / (eventData.Width / eventData.Height); + } else { + h = Math.min(stringToNumber(scaleSel), newSize.height); + w = h * (eventData.Width / eventData.Height); + } + newWidth = parseInt(w); + newHeight = parseInt(h); + currentScale = parseInt(w / eventData.Width * 100); + currentScale = currentScale; + } + + console.log(`Real dimensions: ${eventData.Width} X ${eventData.Height}, Scale: ${currentScale}, deltaScale: ${deltaScale()}, New dimensions: ${newWidth} X ${newHeight}`); + + eventViewer.width(newWidth); + eventViewer.height(newHeight); + if (player) { + if (player.resize) { + console.log(player); + player.resize(newWidth, newHeight); + } + $j('#videoobj canvas').width(newWidth).height(newHeight); + console.log(player); + } else if (!vid) { // zms needs extra sizing + streamScale(currentScale); + drawProgressBar(); + } + if (cueFrames) { + //just re-render alarmCues. skip ajax call + alarmCue.html(renderAlarmCues(videoFeed)); + } + + // After a resize, check if we still have room to display the event stats table + onStatsResize(newWidth); + + //updateScale = true; + + /* OLD version scale = parseFloat($j('#scale').val()); setCookie('zmEventScale'+eventData.MonitorId, scale); @@ -368,8 +432,15 @@ function changeScale() { // After a resize, check if we still have room to display the event stats table onStatsResize(newWidth); + */ } // end function changeScale +function changeStreamQuality() { + const streamQuality = $j('#streamQuality').val(); + setCookie('zmStreamQuality', streamQuality); + streamScale(currentScale); +} + function changeReplayMode() { var replayMode = $j('#replayMode').val(); @@ -463,15 +534,17 @@ function getCmdResponse(respObj, respText) { streamPlay( ); } $j('#progressValue').html(secsToTime(parseInt(streamStatus.progress))); - $j('#zoomValue').html(streamStatus.zoom); - if (streamStatus.zoom == '1.0') { - setButtonState('zoomOutBtn', 'unavail'); - } else { - setButtonState('zoomOutBtn', 'inactive'); - } - if (scale && (streamStatus.scale !== undefined) && (streamStatus.scale != scale)) { - console.log("Stream not scaled, re-applying", scale, streamStatus.scale); - streamScale(scale); + //$j('#zoomValue').html(streamStatus.zoom); + $j('#zoomValue').html(zmPanZoom.panZoom[eventData.MonitorId].getScale().toFixed(1)); + //if (streamStatus.zoom == '1.0') { + // setButtonState('zoomOutBtn', 'unavail'); + //} else { + // setButtonState('zoomOutBtn', 'inactive'); + //} + + if (currentScale && (streamStatus.scale !== undefined) && (streamStatus.scale != currentScale + deltaScale())) { + console.log("Stream not scaled, re-applying", currentScale + deltaScale(), streamStatus.scale); + streamScale(currentScale); } updateProgressBar(streamStatus.progress); @@ -533,8 +606,10 @@ function playClicked( ) { // The assumption is that the command failed because zms exited, so restart the stream. const img = document.getElementById('evtStream'); const src = img.src; + const url = new URL(src); + url.searchParams.set('scale', currentScale); // In event.php we don’t yet know what scale to substitute. Let it be for now. img.src = ''; - img.src = src; + img.src = url; zmsBroke = false; } else { streamReq({command: CMD_PLAY}); @@ -716,6 +791,7 @@ function tagAndPrev(action) { streamPrev(action); } +/* Not used function vjsPanZoom(action, x, y) { //Pan and zoom with centering where the click occurs var outer = $j('#videoobj'); var video = outer.children().first(); @@ -788,13 +864,16 @@ function streamZoomOut() { streamReq({command: CMD_ZOOMOUT}); } } +*/ function streamScale(scale) { + scale += deltaScale(); if (playerType == 'mjpeg') { - streamReq({command: CMD_SCALE, scale: scale}); + streamReq({command: CMD_SCALE, scale: (scale>100) ? 100 : scale}); } } +/* function streamPan(x, y) { if (vid) { vjsPanZoom('pan', x, y); @@ -804,12 +883,15 @@ function streamPan(x, y) { streamReq({command: CMD_PAN, x: x, y: y}); } } +*/ function streamSeek(offset) { if (playerType == 'mjpeg') { streamReq({command: CMD_SEEK, offset: offset}); } else if (playerType == 'h265web.js') { player.seek(offset); + } else if (vid) { + vid.currentTime(offset); } } @@ -842,7 +924,7 @@ function getEventResponse(respObj, respText) { $j('#modeValue').html('Replay'); $j('#zoomValue').html('1'); $j('#rate').val('100'); - vjsPanZoom('zoomOut'); + //vjsPanZoom('zoomOut'); } else { drawProgressBar(); } @@ -973,6 +1055,7 @@ function updateProgressBar(progress) { // Handles seeking when clicking on the progress bar. function progressBarNav() { + console.log('progress'); $j('#progressBar').click(function(e) { let x = e.pageX - $j(this).offset().left; if (x<0) x=0; @@ -1022,34 +1105,70 @@ function progressBarNav() { } function handleClick(event) { - if (vid && (event.target.id != 'videoobj')) { - return; // ignore clicks on control bar - } - // target should be the img tag - if (!(event.ctrlKey && (event.shift || event.shiftKey))) { - const target = $j(event.target); - - const width = target.width(); - const height = target.height(); - - const scaleX = parseFloat(eventData.Width / width); - const scaleY = parseFloat(eventData.Height / height); - const pos = target.offset(); - const x = parseInt((event.pageX - pos.left) * scaleX); - const y = parseInt((event.pageY - pos.top) * scaleY); - - if (event.shift || event.shiftKey) { // handle both jquery and mootools - streamPan(x, y); - updatePrevCoordinatFrame(x, y); //Fixing current coordinates after scaling or shifting - } else if (event.ctrlKey) { // allow zoom out by control click. useful in fullscreen - streamZoomOut(); - } else { - streamZoomIn(x, y); - updatePrevCoordinatFrame(x, y); //Fixing current coordinates after scaling or shifting + if (panZoomEnabled) { + if (!event.target.closest('#wrapperEventVideo')) { + return; + } + + //event.preventDefault(); + const monitorId = eventData.MonitorId; // Event page + //We are looking for an object with an ID, because there may be another element in the button. + const obj = event.target.id ? event.target : event.target.parentElement; + + if (obj.className.includes('btn-zoom-out') || obj.className.includes('btn-zoom-in')) return; + if (obj.className.includes('btn-edit-monitor')) { + const url = '?view=monitor&mid='+monitorId; + if (event.ctrlKey) { + window.open(url, '_blank'); + } else { + window.location.assign(url); + } + } + + const obj_id = obj.getAttribute('id'); + //if (obj.getAttribute('id').indexOf("liveStream") >= 0 || obj.getAttribute('id').indexOf("button_zoom") >= 0) { //Montage & Watch page + if (obj_id && ( + obj_id.indexOf("evtStream") >= 0 || + obj_id.indexOf("button_zoom") >= 0 || + obj.querySelector('video')) + ) { //Event page + //panZoom[monitorId].setOptions({disablePan: false}); + zmPanZoom.click(monitorId); + } + } else { + // +++ Old ZoomPan algorithm. + /* + if (vid && (event.target.id != 'videoobj')) { + return; // ignore clicks on control bar } + // target should be the img tag + if (!(event.ctrlKey && (event.shift || event.shiftKey))) { + const target = $j(event.target); + + const width = target.width(); + const height = target.height(); + + const scaleX = parseFloat(eventData.Width / width); + const scaleY = parseFloat(eventData.Height / height); + const pos = target.offset(); + const x = parseInt((event.pageX - pos.left) * scaleX); + const y = parseInt((event.pageY - pos.top) * scaleY); + + if (event.shift || event.shiftKey) { // handle both jquery and mootools + streamPan(x, y); + updatePrevCoordinatFrame(x, y); //Fixing current coordinates after scaling or shifting + } else if (event.ctrlKey) { // allow zoom out by control click. useful in fullscreen + streamZoomOut(); + } else { + streamZoomIn(x, y); + updatePrevCoordinatFrame(x, y); //Fixing current coordinates after scaling or shifting + } + } + */// --- Old ZoomPan algorithm. } } +/* function shiftImgFrame() { //We calculate the coordinates of the image displacement and shift the image let newPosX = parseInt(PrevCoordinatFrame.x - coordinateMouse.shiftMouse_x); let newPosY = parseInt(PrevCoordinatFrame.y - coordinateMouse.shiftMouse_y); @@ -1083,8 +1202,14 @@ function getCoordinateMouse(event) { //We get the current cursor coordinates tak return {x: parseInt((event.pageX - pos.left) * scaleX), y: parseInt((event.pageY - pos.top) * scaleY)}; //The point of the mouse click relative to the dimensions of the real frame. } +*/ function handleMove(event) { +/* + if (panZoomEnabled) { + return; + } + // +++ Old ZoomPan algorithm. if (event.ctrlKey && (event.shift || event.shiftKey)) { document.ondragstart = function() { return false; @@ -1122,6 +1247,8 @@ function handleMove(event) { updateCoordinateMouse(x, y); leftBtnStatus.UpAfterDown = false; } + // --- Old ZoomPan algorithm. +*/ } // Manage the DELETE CONFIRMATION modal button @@ -1174,12 +1301,16 @@ function getStat() { //switch ( ( eventData[key] && eventData[key].length ) ? key : 'n/a') { switch (key) { - case 'Frames': - tdString = '' + eventData[key] + ''; + case 'Name': + tdString = eventData[key] + ' (Id:' + eventData['Id'] + ')'; break; - case 'AlarmFrames': + case 'Frames': tdString = '' + eventData[key] + ''; + tdString += ' Alarm:' + '' + eventData['AlarmFrames'] + ''; break; + //case 'AlarmFrames': + // tdString = '' + eventData[key] + ''; + // break; case 'Location': tdString = eventData.Latitude + ', ' + eventData.Longitude; break; @@ -1202,8 +1333,11 @@ function getStat() { tdString = eventData[key]; } break; - case 'MaxScore': - tdString = '' + eventData[key] + ''; + //case 'MaxScore': + // tdString = '' + eventData[key] + ''; + // break; + case 'Score': + tdString = 'Total:' + eventData['TotScore'] + ' '+ '' + 'Max:' + eventData['MaxScore'] + '' + ' Avg:' + eventData['AvgScore']; break; case 'n/a': tdString = 'n/a'; @@ -1211,12 +1345,19 @@ function getStat() { case 'Resolution': tdString = eventData.Width + 'x' + eventData.Height; break; + case 'DiskSpace': + tdString = eventData[key] + ' on ' + eventData['Storage']; + break; case 'Path': tdString = ''+eventData.Path+''; break; - case 'Archived': - case 'Emailed': - tdString = eventData[key] ? yesStr : noStr; + //case 'Archived': + //case 'Emailed': + // tdString = eventData[key] ? yesStr : noStr; + // break; + case 'Info': + tdString = translate["Archived"] + ':' + (eventData['Archived'] ? yesStr : noStr); + tdString += ', ' + translate["Emailed"] + ':' + (eventData['Emailed'] ? yesStr : noStr); break; case 'Length': const date = new Date(0); // Have to init it fresh. setSeconds seems to add time, not set it. @@ -1238,11 +1379,11 @@ function getStat() { function onStatsResize(vidWidth) { if (!vidWidth) return; const minWidth = 200; // An arbitrary value in pixels used to hide the stats table - const scale = $j('#scale').val(); + //const scale = $j('#scale').val(); - if (parseInt(scale)) { - vidWidth = vidWidth * (scale/100); - } + //if (parseInt(scale)) { + // vidWidth = vidWidth * (scale/100); + //} const width = $j(window).width() - vidWidth; //console.log("Width: " + width + " = window.width " + $j(window).width() + "- vidWidth" + vidWidth); @@ -1272,6 +1413,8 @@ function initPage() { // Load the event stats getStat(); + zmPanZoom.init(); + changeStreamQuality(); if (getEvtStatsCookie() != 'on') { eventStats.toggle(false); @@ -1280,7 +1423,7 @@ function initPage() { onStatsResize(eventData.Width); wrapperEventVideo.removeClass('col-sm-12').addClass('col-sm-8'); } - if (scale == '0') changeScale(); + changeScale(); progressBarNav(); @@ -1294,8 +1437,8 @@ function initPage() { } { - let vid = document.createElement('video'); if ( codec != 'mjpeg') { + let vid = document.createElement('video'); if (vid.canPlayType('video/mp4; codecs="'+codec+'"')) { console.log("can play " + codec); } else { @@ -1303,7 +1446,6 @@ function initPage() { } } } - //FIXME prevent blocking...not sure what is happening or best way to unblock if (playerType == 'h265web.js') { if (!(eventData.DefaultVideo.indexOf('h265') >= 0 || eventData.DefaultVideo.indexOf('hevc') >= 0)) console.log("Warning, using h265web.js on a non-h265 file"); @@ -1311,53 +1453,54 @@ function initPage() { const PLAYER_CORE_TYPE_CNATIVE = 1; const token = "base64:QXV0aG9yOmNoYW5neWFubG9uZ3xudW1iZXJ3b2xmLEdpdGh1YjpodHRwczovL2dpdGh1Yi5jb20vbnVtYmVyd29sZixFbWFpbDpwb3JzY2hlZ3QyM0Bmb3htYWlsLmNvbSxRUTo1MzEzNjU4NzIsSG9tZVBhZ2U6aHR0cDovL3h2aWRlby52aWRlbyxEaXNjb3JkOm51bWJlcndvbGYjODY5NCx3ZWNoYXI6bnVtYmVyd29sZjExLEJlaWppbmcsV29ya0luOkJhaWR1"; - const evtStream = $j('#evtStream'); + console.log('initial width and height', newWidth, newHeight); + const evtStream = $j('#videoFeed'); const config = { player: 'videoobj', - width: evtStream.width(), - height: evtStream.height(), + width: newWidth, + height: newHeight, //accurateSeek: true, token: token, extInfo: { - //probeSize : 8192, + probeSize : 8192, autoPlay : true, moovStartFlag: true, readyShow: true, //core: PLAYER_CORE_TYPE_DEFAULT, - //core : PLAYER_CORE_TYPE_CNATIVE, + core : PLAYER_CORE_TYPE_CNATIVE, //cacheLength : 50, - coreProbePart: 0.4, //0.1 didn't work + coreProbePart: 1.0, //0.1 didn't work // anything less than 1 doesn't load the whole file. ignoreAudio: 0 } }; player = window.new265webjs(videoUrl+'&ext=.mp4', config); - const progressVoice = document.querySelector('#volume'); - progressVoice.addEventListener('click', (e) => { - let x = e.pageX - progressVoice.getBoundingClientRect().left; // or e.offsetX (less support, though) - let y = e.pageY - progressVoice.getBoundingClientRect().top; // or e.offsetY - let clickedValue = x * progressVoice.max / progressVoice.offsetWidth; - progressVoice.value = clickedValue; + const volume = document.querySelector('#volume'); + volume.addEventListener('click', (e) => { + let x = e.pageX - volume.getBoundingClientRect().left; // or e.offsetX (less support, though) + let y = e.pageY - volume.getBoundingClientRect().top; // or e.offsetY + let clickedValue = x * volume.max / volume.offsetWidth; + volume.value = clickedValue; let volume = clickedValue / 100; // alert(volume); // console.log( - // progressVoice.offsetLeft, // 209 + // volume.offsetLeft, // 209 // x, y, // 324 584 - // progressVoice.max, progressVoice.offsetWidth); + // volume.max, volume.offsetWidth); player.setVoice(volume); }); const showLabel = document.querySelector('#showLabel'); - const coverToast = document.querySelector('#coverLayer'); - const coverBtn = document.querySelector('#coverLayerBtn'); + //const coverToast = document.querySelector('#coverLayer'); + //const coverBtn = document.querySelector('#coverLayerBtn'); let muteState = false; const muteBtn = document.querySelector('#muteBtn'); muteBtn.onclick = () => { if (muteState === true) { player.setVoice(1.0); - progressVoice.value = 100; + volume.value = 100; } else { player.setVoice(0.0); - progressVoice.value = 0; + volume.value = 0; } muteState = !muteState; }; @@ -1416,7 +1559,7 @@ function initPage() { console.log("mediaInfo===========>", mediaInfo); if (mediaInfo.meta.isHEVC === false) { console.log("is not HEVC/H.265 media!"); - //coverToast.removeAttribute('hidden'); + ////coverToast.removeAttribute('hidden'); //coverBtn.style.width = '100%'; //coverBtn.style.fontSize = '50px'; //coverBtn.innerHTML = 'is not HEVC/H.265 media!'; @@ -1425,13 +1568,13 @@ function initPage() { //console.log("is HEVC/H.265 media."); if (mediaInfo.meta.audioNone) { - progressVoice.value = 0; - progressVoice.style.display = 'none'; + volume.value = 0; + volume.style.display = 'none'; } else { let volume = getCookie('volume'); if (volume !== null) { player.setVoice(volume/100); - progressVoice.value = volume; + volume.value = volume; } } if (mediaInfo.videoType == "vod") { @@ -1441,17 +1584,18 @@ function initPage() { if (mediaInfo.meta.audioNone === true) { player.play(); } else { - coverToast.removeAttribute('hidden'); - coverBtn.onclick = () => { + //coverToast.removeAttribute('hidden'); + //coverBtn.onclick = () => { // playBar.textContent = '||'; - playAction(); - coverToast.setAttribute('hidden', 'hidden'); - }; + //playAction(); + //coverToast.setAttribute('hidden', 'hidden'); + //}; } } + player.play(); showLabel.textContent = SHOW_DONE; - }; + }; // onLoadFinish player.do(); } else if (playerType == 'video.js') { //FIXME prevent blocking...not sure what is happening or best way to unblock @@ -1499,7 +1643,8 @@ function initPage() { if (!streamImg) { streamImg = $j('#imageFeed object'); } - $j(streamImg).click(function(event) { + const observedObject = panZoomEnabled ? 'body' : streamImg; + $j(observedObject).click(function(event) { handleClick(event); }); $j(streamImg).mousemove(function(event) { @@ -1510,6 +1655,7 @@ function initPage() { } else { console.error("Unknown playerType: " + playerType); } // end if playerType + progressBarNav(); nearEventsQuery(eventData.Id); initialAlarmCues(eventData.Id); //call ajax+renderAlarmCues document.querySelectorAll('select[name="rate"]').forEach(function(el) { @@ -1752,6 +1898,14 @@ function initPage() { removeTag(tag); }); + // Event listener for double click + //var elStream = document.querySelectorAll('[id ^= "liveStream"], [id ^= "evtStream"]'); + var elStream = document.querySelectorAll('[id = "wrapperEventVideo"]'); + Array.prototype.forEach.call(elStream, (el) => { + el.addEventListener('touchstart', doubleTouch); + el.addEventListener('dblclick', doubleClickOnStream); + }); + streamPlay(); if ( parseInt(ZM_OPT_USE_GEOLOCATION) && parseFloat(eventData.Latitude) && parseFloat(eventData.Longitude)) { @@ -1783,6 +1937,33 @@ function initPage() { console.log('Location turned on but leaflet not installed.'); } } // end if ZM_OPT_USE_GEOLOCATION + + $j("#videoFeed").hover( + //Displaying "Scale" and other buttons at the top of the monitor image + function() { + //const id = stringToNumber(this.id); //Montage & Watch page + const id = eventData.MonitorId; // Event page + $j('#button_zoom' + id).stop(true, true).slideDown('fast'); + }, + function() { + //const id = stringToNumber(this.id); //Montage & Watch page + const id = eventData.MonitorId; // Event page + $j('#button_zoom' + id).stop(true, true).slideUp('fast'); + } + ); + + setInterval(() => { + //Updating Scale. When quickly scrolling the mouse wheel or quickly pressing Zoom In/Out, you should not set Scale very often. + if (updateScale) { + const eventViewer = $j(vid ? '#videoobj' : '#evtStream'); + const panZoomScale = panZoomEnabled ? zmPanZoom.panZoom[eventData.MonitorId].getScale() : 1; + const newSize = scaleToFit(eventData.Width, eventData.Height, eventViewer, false, $j('#videoFeed'), panZoomScale); + scale = newSize.autoScale > 100 ? 100 : newSize.autoScale; + currentScale = scale; + streamScale(currentScale); + updateScale = false; + } + }, 500); } // end initPage function addOrCreateTag(tagValue) { @@ -1955,5 +2136,13 @@ function fullscreenClicked() { } } +function panZoomIn(el) { + zmPanZoom.zoomIn(el); +} + +function panZoomOut(el) { + zmPanZoom.zoomOut(el); +} + // Kick everything off $j( window ).on("load", initPage); diff --git a/web/skins/classic/views/js/event.js.php b/web/skins/classic/views/js/event.js.php index d7f60d6989..cc7bad4944 100644 --- a/web/skins/classic/views/js/event.js.php +++ b/web/skins/classic/views/js/event.js.php @@ -45,7 +45,6 @@ Archived: Archived?'true':'false' ?>, Emailed: Emailed?'true':'false' ?>, DefaultVideo: 'DefaultVideo() ?>', - Path: 'Path() ?>' Path: 'Path() ?>', Latitude: 'Latitude() ?>', Longitude: 'Longitude() ?>' @@ -56,7 +55,7 @@ var noStr = ''; var eventDataStrings = { - Id: '', + Name: '', MonitorId: '', MonitorName: '', @@ -68,16 +67,18 @@ EndDateTimeFormatted: '', Length: '', Frames: '', - AlarmFrames: '', - TotScore: '', - AvgScore: '', - MaxScore: '', + + + + + Score: '', Resolution: '', DiskSpace: '', - Storage: '', + Path: '', - Archived: '', - Emailed: '' + + + Info: '' }; if ( parseInt(ZM_OPT_USE_GEOLOCATION) ) { eventDataStrings.Location = ''; @@ -118,4 +119,7 @@ "Live": "", "Edit": "", "All Events": "", + "Info": "", + "Archived": "", + "Emailed": "", }; diff --git a/web/skins/classic/views/js/events.js b/web/skins/classic/views/js/events.js index 4e4203fa7c..6c553e9952 100644 --- a/web/skins/classic/views/js/events.js +++ b/web/skins/classic/views/js/events.js @@ -1,12 +1,13 @@ -var backBtn = $j('#backBtn'); -var viewBtn = $j('#viewBtn'); -var archiveBtn = $j('#archiveBtn'); -var unarchiveBtn = $j('#unarchiveBtn'); -var editBtn = $j('#editBtn'); -var exportBtn = $j('#exportBtn'); -var downloadBtn = $j('#downloadBtn'); -var deleteBtn = $j('#deleteBtn'); -var table = $j('#eventTable'); +"use strict"; +const backButton = $j('#backBtn'); +const viewButton = $j('#viewBtn'); +const archiveButton = $j('#archiveBtn'); +const unarchiveButton = $j('#unarchiveBtn'); +const editButton = $j('#editBtn'); +const exportButton = $j('#exportBtn'); +const downloadButton = $j('#downloadBtn'); +const deleteButton = $j('#deleteBtn'); +const table = $j('#eventTable'); var ajax = null; /* @@ -60,7 +61,9 @@ function ajaxRequest(params) { params.success({total: data.total, totalNotFiltered: data.totalNotFiltered, rows: rows}); }, error: function(jqXHR) { - console.log("error", jqXHR); + if (jqXHR.statusText != 'abort') { + console.log("error", jqXHR); + } //logAjaxFail(jqXHR); //$j('#eventTable').bootstrapTable('refresh'); } @@ -86,6 +89,9 @@ function processRows(rows) { row.Frames = '' + row.Frames + ''; row.AlarmFrames = '' + row.AlarmFrames + ''; row.MaxScore = '' + row.MaxScore + ''; + row.Location = row.Latitude+', '+row.Longitude; + row.DiskSpace = '' + row.DiskSpace + ''; + row.Storage = '' + row.Storage + ''; const date = new Date(0); // Have to init it fresh. setSeconds seems to add time, not set it. date.setSeconds(row.Length); @@ -124,8 +130,8 @@ function getIdSelections() { // Returns a boolen to indicate at least one selected row is archived function getArchivedSelections() { - var table = $j('#eventTable'); - var selection = $j.map(table.bootstrapTable('getSelections'), function(row) { + const table = $j('#eventTable'); + const selection = $j.map(table.bootstrapTable('getSelections'), function(row) { return row.Archived; }); return selection.includes('Yes'); @@ -314,19 +320,29 @@ function initPage() { table.on('check.bs.table uncheck.bs.table ' + 'check-all.bs.table uncheck-all.bs.table', function() { - selections = table.bootstrapTable('getSelections'); - - viewBtn.prop('disabled', !(selections.length && canView.Events)); - archiveBtn.prop('disabled', !(selections.length && canEdit.Events)); - unarchiveBtn.prop('disabled', !(getArchivedSelections()) && canEdit.Events); - editBtn.prop('disabled', !(selections.length && canEdit.Events)); - exportBtn.prop('disabled', !(selections.length && canView.Events)); - downloadBtn.prop('disabled', !(selections.length && canView.Events)); - deleteBtn.prop('disabled', !(selections.length && canEdit.Events)); + const selections = table.bootstrapTable('getSelections'); + + viewButton.prop('disabled', !(selections.length && canView.Events)); + archiveButton.prop('disabled', !(selections.length && canEdit.Events)); + if (!(getArchivedSelections() && canEdit.Events)) { + unarchiveButton.prop('disabled', true); + if (!getArchivedSelections()) { + unarchiveButton.prop('title', 'Please select an event that is archived.'); + } else { + unarchiveButton.prop('title', 'You must have events edit permission to unarchive'); + } + } else { + unarchiveButton.prop('disabled', false); + unarchiveButton.prop('title', unarchiveString); + } + editButton.prop('disabled', !(selections.length && canEdit.Events)); + exportButton.prop('disabled', !(selections.length && canView.Events)); + downloadButton.prop('disabled', !(selections.length && canView.Events)); + deleteButton.prop('disabled', !(selections.length && canEdit.Events)); }); // Don't enable the back button if there is no previous zm page to go back to - backBtn.prop('disabled', !document.referrer.length); + backButton.prop('disabled', !document.referrer.length); // Setup the thumbnail video animation if (!isMobile()) initThumbAnimation(); @@ -518,7 +534,10 @@ function initPage() { table.show(); } -function filterEvents() { +function filterEvents(clickedElement) { + if (clickedElement.target && clickedElement.target.id == 'filterArchived') { + setCookie('zmFilterArchived', clickedElement.target.value); + } filterQuery = ''; $j('#fieldsTable input').each(function(index) { const el = $j(this); diff --git a/web/skins/classic/views/js/events.js.php b/web/skins/classic/views/js/events.js.php index 207a6745e7..52a39ca41a 100644 --- a/web/skins/classic/views/js/events.js.php +++ b/web/skins/classic/views/js/events.js.php @@ -6,9 +6,12 @@ var filterQuery = ''; var sortQuery = ''; -var confirmDeleteEventsString = ""; -var archivedString = ""; -var emailedString = ""; -var yesString = ""; -var noString = ""; +const confirmDeleteEventsString = ""; +const archivedString = ""; +const unarchivedString = ""; +const archiveString = ""; +const unarchiveString = ""; +const emailedString = ""; +const yesString = ""; +const noString = ""; var WEB_LIST_THUMBS = ; diff --git a/web/skins/classic/views/js/frame.js b/web/skins/classic/views/js/frame.js index b1b956e732..5fc9da4048 100644 --- a/web/skins/classic/views/js/frame.js +++ b/web/skins/classic/views/js/frame.js @@ -54,6 +54,12 @@ function getStat(params) { $j.getJSON(thisUrl + '?view=request&request=stats&raw=true', params) .done(function(data) { $j('#frameStatsTable').empty().append(''); + if (data.result == "Error") { + console.log(`Error running getStat function: ${data.message}`); + statsBtn.prop('disabled', true); + statsBtn.prop('title', 'No statistics available for this frame'); + return; + } if (!data.raw.length) { statsBtn.prop('disabled', true); statsBtn.prop('title', 'No statistics available for this frame'); diff --git a/web/skins/classic/views/js/monitor.js b/web/skins/classic/views/js/monitor.js index e788322413..3c43681b7d 100644 --- a/web/skins/classic/views/js/monitor.js +++ b/web/skins/classic/views/js/monitor.js @@ -136,6 +136,9 @@ function initPage() { } }; }); + document.querySelectorAll('select[name="newMonitor[Devices]"]').forEach(function(el) { + el.onchange = window['devices_onchange'].bind(el, el); + }); document.querySelectorAll('input[name="newMonitor[Width]"]').forEach(function(el) { el.oninput = window['updateMonitorDimensions'].bind(el, el); }); @@ -167,8 +170,10 @@ function initPage() { el.onchange = function() { if (this.value == 1 /* Encode */) { $j('.OutputCodec').show(); + $j('.WallClockTimeStamps').hide(); $j('.Encoder').show(); } else { + $j('.WallClockTimeStamps').show(); $j('.OutputCodec').hide(); $j('.Encoder').hide(); } @@ -391,6 +396,9 @@ function initPage() { } // end if ZM_OPT_USE_GEOLOCATION updateLinkedMonitorsUI(); + + // Setup the thumbnail video animation + if (!isMobile()) initThumbAnimation(); } // end function initPage() function ll2dms(input) { @@ -510,9 +518,11 @@ function random_WebColour() { function buffer_setting_oninput(e) { const max_image_buffer_count = document.getElementById('newMonitor[MaxImageBufferCount]'); const pre_event_count = document.getElementById('newMonitor[PreEventCount]'); - if (parseInt(pre_event_count.value) > parseInt(max_image_buffer_count.value)) { - if (this.id=='newMonitor[PreEventCount]') { - max_image_buffer_count.value=pre_event_count.value; + if (parseInt(max_image_buffer_count.value) && + (parseInt(pre_event_count.value) > parseInt(max_image_buffer_count.value)) + ) { + if (this.id == 'newMonitor[PreEventCount]') { + max_image_buffer_count.value = pre_event_count.value; } else { pre_event_count.value = max_image_buffer_count.value; } @@ -674,4 +684,15 @@ function updateLinkedMonitorsUI() { expr_to_ui($j('[name="newMonitor[LinkedMonitors]"]').val(), $j('#LinkedMonitorsUI')); } +function devices_onchange(devices) { + const selected = $j(devices).val(); + const device = devices.form.elements['newMonitor[Device]']; + if (selected !== '') { + device.value = selected; + device.style['display'] = 'none'; + } else { + device.style['display'] = 'inline'; + } +} + window.addEventListener('DOMContentLoaded', initPage); diff --git a/web/skins/classic/views/js/monitor.js.php b/web/skins/classic/views/js/monitor.js.php index c6392f4f42..c57b2d1ed4 100644 --- a/web/skins/classic/views/js/monitor.js.php +++ b/web/skins/classic/views/js/monitor.js.php @@ -143,7 +143,7 @@ function validateForm(form) { errors[errors.length] = ""; if ( !form.elements['newMonitor[StreamReplayBuffer]'].value || !(parseInt(form.elements['newMonitor[StreamReplayBuffer]'].value) >= 0 ) ) errors[errors.length] = ""; - if (form.elements['newMonitor[MaxImageBufferCount]'].value && (parseInt(form.elements['newMonitor[PreEventCount]'].value) > parseInt(form.elements['newMonitor[MaxImageBufferCount]'].value))) + if (parseInt(form.elements['newMonitor[MaxImageBufferCount]'].value) && (parseInt(form.elements['newMonitor[PreEventCount]'].value) > parseInt(form.elements['newMonitor[MaxImageBufferCount]'].value))) errors[errors.length] = ""; if ( !form.elements['newMonitor[AlarmFrameCount]'].value || !(parseInt(form.elements['newMonitor[AlarmFrameCount]'].value) > 0 ) ) diff --git a/web/skins/classic/views/js/montage.js b/web/skins/classic/views/js/montage.js index d4e044fff9..5ffed84bbe 100644 --- a/web/skins/classic/views/js/montage.js +++ b/web/skins/classic/views/js/montage.js @@ -13,12 +13,9 @@ var objGridStack; var layoutColumns = 48; //Maximum number of columns (items per row) for GridStack var changedMonitors = []; //Monitor IDs that were changed in the DOM -var panZoomEnabled = true; //Add it to settings in the future -var panZoomMaxScale = 10; -var panZoomStep = 0.3; -var panZoom = []; -var shifted; -var ctrled; +var scrollBbarExists = null; +var movableMonitorData = []; //Monitor data (id, width, stop (true - stop moving)) +var TimerHideShow = null; const presetRatio = new Map([ ['auto', ''], @@ -48,12 +45,8 @@ var defaultPresetRatio = 'auto'; var averageMonitorsRatio; -function stringToNumber(str) { - return parseInt(str.replace(/\D/g, '')); -} - function isPresetLayout(name) { - return (( name=='Freeform' || name=='1 Wide' || name=='2 Wide' || name=='3 Wide' || name=='4 Wide' || name=='6 Wide' || name=='8 Wide' || name=='12 Wide' || name=='16 Wide' ) ? true : false); + return ((ZM_PRESET_LAYOUT_NAMES.indexOf(name) != -1) ? true : false); } function getCurrentNameLayout() { @@ -110,6 +103,10 @@ function playClicked() { * @param {*} new_layout_id - the id of a layout to switch to */ function selectLayout(new_layout_id) { + if (mode == EDITING) { + changedMonitors.length = 0; + return; + } const ddm = $j('#zmMontageLayout'); if (new_layout_id && (typeof(new_layout_id) != 'object')) { ddm.val(new_layout_id); @@ -117,12 +114,14 @@ function selectLayout(new_layout_id) { const layout_id = parseInt(ddm.val()); if (!layout_id) { console.log("No layout_id?!"); + changedMonitors.length = 0; return; } const layout = layouts[layout_id]; if (!layout) { console.log("No layout found for " + layout_id); + changedMonitors.length = 0; return; } @@ -134,6 +133,7 @@ function selectLayout(new_layout_id) { } if (isPresetLayout(nameLayout)) { //PRESET + document.getElementById("btnDeleteLayout").setAttribute('disabled', ''); setSelected(document.getElementById("ratio"), getCookie('zmMontageRatioForAll')); changeRatioForAll(); @@ -147,14 +147,16 @@ function selectLayout(new_layout_id) { } const monitor_wrapper = monitor_frame.closest('[gs-id="' + monitor.id + '"]'); - if (nameLayout == "Freeform") { - monitor_wrapper.attr('gs-w', 12).removeAttr('gs-x').removeAttr('gs-y').removeAttr('gs-h'); + if (nameLayout == 'Auto') { + monitor_wrapper.attr('gs-w', layoutColumns / stringToNumber(autoLayoutName)).removeAttr('gs-x').removeAttr('gs-y').removeAttr('gs-h'); + //monitor_wrapper.attr('gs-w', 12).removeAttr('gs-x').removeAttr('gs-y').removeAttr('gs-h'); } else { monitor_wrapper.attr('gs-w', widthFrame).removeAttr('gs-x').removeAttr('gs-y').removeAttr('gs-h'); } } initGridStack(); } else { //CUSTOM + document.getElementById("btnDeleteLayout").removeAttribute('disabled'); for (let i = 0, length = monitors.length; i < length; i++) { const monitor = monitors[i]; // Need to clear the current positioning, and apply the new @@ -293,21 +295,16 @@ function setSelectedRatioForAllMonitors(value) { function changeRatioForAll() { const value = getSelected(document.getElementById("ratio")); - //objGridStack.compact('list', true); //??? - //selectLayout(); //??? - setCookie('zmMontageRatioForAll', value); setSelectedRatioForAllMonitors(value); setTriggerChangedMonitors(); + waitingMonitorsPlaced('changeRatio'); } /*Called from a form*/ function changeRatio(el) { const objSelect = el.target; - //objGridStack.compact('list', true); //??? - //selectLayout(); //??? - checkRatioForAllMonitors(); setTriggerChangedMonitors(stringToNumber(objSelect.id)); } @@ -344,6 +341,27 @@ function checkRatioForAllMonitors() { } } +function setRatioForMonitor(objLiveStream, id=null) { + if (!id) { + id = stringToNumber(objLiveStream.id); + } + const value = getSelected(document.getElementById("ratio"+id)); + const currentMonitor = monitors.find((o) => { + return parseInt(o["id"]) === id; + }); + + var ratio; + if (value == 'real') { + ratio = (currentMonitor.width / currentMonitor.height > 1) ? currentMonitor.width / currentMonitor.height : currentMonitor.height / currentMonitor.width; + } else { + const partsRatio = value.split(':'); + ratio = (value == 'auto') ? averageMonitorsRatio : partsRatio[0]/partsRatio[1]; + } + const height = (currentMonitor.width / currentMonitor.height > 1) ? (objLiveStream.clientWidth / ratio + 'px') /* landscape */ : (objLiveStream.clientWidth * ratio + 'px'); + objLiveStream.style['height'] = height; + objLiveStream.parentNode.style['height'] = height; +} + function toGrid(value) { //Not used /* return Math.round(value / 80) * 80;*/ } @@ -364,7 +382,7 @@ function edit_layout(button) { const monitor = monitors[i]; monitor.disable_onclick(); if (panZoomEnabled) { - panZoomAction('disable', {id: monitors[i].id}); //Disable zoom and pan + zmPanZoom.action('disable', {id: monitors[i].id}); //Disable zoom and pan } }; @@ -378,8 +396,6 @@ function edit_layout(button) { } // end function edit_layout function save_layout(button) { - mode = VIEWING; - const form = button.form; let name = form.elements['Name'].value; const layout = layouts[form.zmMontageLayout.value]; @@ -398,6 +414,8 @@ function save_layout(button) { return; } + mode = VIEWING; + var Positions = {}; Positions['gridStack'] = objGridStack.save(false, false); Positions['monitorStatusPositon'] = $j('#monitorStatusPositon').val(); //Not yet used when reading Layout @@ -406,6 +424,7 @@ function save_layout(button) { Positions['monitorRatio'][stringToNumber(this.id)] = getSelected(this); }); form.Positions.value = JSON.stringify(Positions, null, ' '); + $j('#action').attr('value', 'Save'); form.submit(); } // end function save_layout @@ -420,7 +439,7 @@ function cancel_layout(button) { if (panZoomEnabled) { $j('.zoompan').each( function() { - panZoomAction('enable', {obj: this}); //Enable zoom and pan + zmPanZoom.action('enable', {obj: this}); //Enable zoom and pan }); } @@ -433,12 +452,59 @@ function cancel_layout(button) { selectLayout(); } +function delete_layout(button) { + if (!canEdit.System) { + enoperm(); + return; + } + if (!document.getElementById('deleteConfirm')) { + // Load the delete confirmation modal into the DOM + // $j.getJSON(thisUrl + '?request=modal&modal=delconfirm') + $j.getJSON(thisUrl + '?request=modal&modal=delconfirm', { + key: 'ConfirmDeleteLayout', + }) + .done(function(data) { + insertModalHtml('deleteConfirm', data.html); + manageDelConfirmModalBtns(); + $j('#deleteConfirm').modal('show'); + }) + .fail(function(jqXHR) { + console.log('error getting delconfirm', jqXHR); + logAjaxFail(jqXHR); + }); + return; + } else { + $j('#deleteConfirm').modal('show'); + } +} // end function delete_layout + +// Manage the DELETE CONFIRMATION modal button +function manageDelConfirmModalBtns() { + document.getElementById('delConfirmBtn').addEventListener('click', function onDelConfirmClick(evt) { + document.getElementById('delConfirmBtn').disabled = true; // prevent double click + if (!canEdit.Monitors) { + enoperm(); + return; + } + evt.preventDefault(); + + const form = $j('#btnDeleteLayout')[0].form; + $j('#action').attr('value', 'Delete'); + form.submit(); + }); + + // Manage the CANCEL modal button + document.getElementById('delCancelBtn').addEventListener('click', function onDelCancelClick(evt) { + $j('#deleteConfirm').modal('hide'); + }); +} + function reloadWebSite(ndx) { document.getElementById('imageFeed'+ndx).innerHTML = document.getElementById('imageFeed'+ndx).innerHTML; } function takeSnapshot() { - for (let i = 0, length = monitorData.length; i < length; i++) { + for (let i = 0, length = monitors.length; i < length; i++) { monitors[i].kill(); } const monitor_ids = monitorData.map((monitor)=>{ @@ -486,22 +552,19 @@ function handleClick(evt) { if (obj.getAttribute('id').indexOf("liveStream") >= 0) { id = stringToNumber(obj.getAttribute('id')); - - if (ctrled && shifted) { - return; - } else if (ctrled) { - panZoom[id].zoom(1, {animate: true}); - } else if (shifted) { - const scale = panZoom[id].getScale() * Math.exp(panZoomStep); - const point = {clientX: event.clientX, clientY: event.clientY}; - panZoom[id].zoomToPoint(scale, point, {focal: {x: event.clientX, y: event.clientY}}); - } - //updateScale = true; + zmPanZoom.click(id); } } function startMonitors() { - for (let i = 0, length = monitorData.length; i < length; i++) { + for (let i = 0, length = monitors.length; i < length; i++) { + const obj = document.getElementById('liveStream'+monitors[i].id); + if (obj.src) { + const url = new URL(obj.src); + url.searchParams.set('scale', parseInt(obj.clientWidth / monitors[i].width * 100)); + obj.src = url; + } + // Start the fps and status updates. give a random delay so that we don't assault the server const delay = Math.round( (Math.random()+0.5)*statusRefreshTimeout ); monitors[i].start(delay); @@ -513,7 +576,7 @@ function startMonitors() { } function stopMonitors() { //Not working yet. - for (let i = 0, length = monitorData.length; i < length; i++) { + for (let i = 0, length = monitors.length; i < length; i++) { //monitors[i].stop(); //monitors[i].kill(); monitors[i].streamCommand(CMD_QUIT); @@ -522,13 +585,13 @@ function stopMonitors() { //Not working yet. } function pauseMonitors() { - for (let i = 0, length = monitorData.length; i < length; i++) { + for (let i = 0, length = monitors.length; i < length; i++) { monitors[i].pause(); } } function playMonitors() { - for (let i = 0, length = monitorData.length; i < length; i++) { + for (let i = 0, length = monitors.length; i < length; i++) { monitors[i].play(); } } @@ -583,7 +646,7 @@ function fullscreenchanged(event) { objBtn.children('.material-icons').html('fullscreen'); } //Sometimes the positioning is not correct, so it is better to reset Pan & Zoom - panZoom[stringToNumber(event.target.id)].reset(); + zmPanZoom.panZoom[stringToNumber(event.target.id)].reset(); } } @@ -605,33 +668,6 @@ function calculateAverageMonitorsRatio(arrRatioMonitors) { }); } -/* -* Id - Monitor ID -* The function will probably be moved to the main JS file -*/ -function manageCursor(Id) { - const obj = document.getElementById('liveStream'+Id); - const currentScale = panZoom[Id].getScale().toFixed(1); - - if (shifted && ctrled) { - obj.closest('.zoompan').style['cursor'] = 'not-allowed'; - } else if (shifted) { - obj.closest('.zoompan').style['cursor'] = 'zoom-in'; - } else if (ctrled) { - if (currentScale == 1.0) { - obj.closest('.zoompan').style['cursor'] = 'auto'; - } else { - obj.closest('.zoompan').style['cursor'] = 'zoom-out'; - } - } else { - if (currentScale == 1.0) { - obj.closest('.zoompan').style['cursor'] = 'auto'; - } else { - obj.closest('.zoompan').style['cursor'] = 'move'; - } - } -} - function initPage() { monitors_ul = $j('#monitors'); @@ -653,9 +689,9 @@ function initPage() { $j("#flipMontageHeader").slideToggle("fast"); $j("#hdrbutton").toggleClass('glyphicon-menu-down').toggleClass('glyphicon-menu-up'); } - if (getCookie('zmMontageLayout')) { - $j('#zmMontageLayout').val(getCookie('zmMontageLayout')); - } + //if (getCookie('zmMontageLayout')) { //This is implemented in montage.php And the cookies may contain the number of a non-existent Layouts!!! + // $j('#zmMontageLayout').val(getCookie('zmMontageLayout')); + //} $j(".grid-monitor").hover( //Displaying "Scale" and other buttons at the top of the monitor image @@ -683,10 +719,12 @@ function initPage() { //Create a Ratio array for each monitor const r = monitors[i].width / monitors[i].height; arrRatioMonitors.push(r > 1 ? r : 1/r); //landscape or portret orientation + + //Prepare the array. + movableMonitorData[monitors[i].id] = {'width': 0, 'stop': false}; } calculateAverageMonitorsRatio(arrRatioMonitors); - startMonitors(); $j(window).on('resize', windowResize); //Only used when trying to apply "changeScale". It will be deleted in the future. document.addEventListener("fullscreenchange", fullscreenchanged); @@ -732,56 +770,34 @@ function initPage() { setInterval(() => { //Updating GridStack resizeToContent, Scale & Ratio if (changedMonitors.length > 0) { - changedMonitors.forEach(function(item, index, object) { - const value = getSelected(document.getElementById("ratio"+item)); + changedMonitors.slice().reverse().forEach(function(item, index, object) { const img = document.getElementById('liveStream'+item); - const currentMonitor = monitors.find((o) => { - return parseInt(o["id"]) === item; - }); - if (value == 'real') { - img.style['height'] = 'auto'; - img.parentNode.style['height'] = 'auto'; - } else { - const partsRatio = value.split(':'); - const monitorRatioSel = partsRatio[0]/partsRatio[1]; - const ratio = (value == 'auto') ? averageMonitorsRatio : monitorRatioSel; - const h = (currentMonitor.width / currentMonitor.height > 1) ? (img.clientWidth / ratio + 'px') /*landscape*/ : (img.clientWidth * ratio + 'px'); - img.style['height'] = h; - img.parentNode.style['height'] = h; - } - if (img.offsetHeight > 20 && objGridStack) { //Required for initial page loading + setRatioForMonitor(img, item); objGridStack.resizeToContent(document.getElementById('m'+item)); - changedMonitors.splice(index, 1); + changedMonitors.splice(object.length - 1 - index, 1); } monitorsSetScale(item); }); } - }, 200); + }, 100); - setTimeout(() => { - $j('#monitors').removeClass('hidden-shift'); - selectLayout(); - }, 50); //No matter what flickers. But perhaps this will not be necessary in the future... + selectLayout(); + $j('#monitors').removeClass('hidden-shift'); changeMonitorStatusPositon(); - - if (panZoomEnabled) { - $j('.zoompan').each( function() { - panZoomAction('enable', {obj: this}); - const id = stringToNumber(this.querySelector("[id^='liveStream']").id); - $j(document).on('keyup keydown', function(e) { - shifted = e.shiftKey ? e.shiftKey : e.shift; - ctrled = e.ctrlKey; - manageCursor(id); - }); - this.addEventListener('mousemove', function(e) { - //Temporarily not use - }); - }); - } + zmPanZoom.init(); // Creating a ResizeObserver Instance const observer = new ResizeObserver((objResizes) => { + const blockContent = document.getElementById('content'); + const currentScrollBbarExists = blockContent.scrollHeight > blockContent.clientHeight; + if (scrollBbarExists === null) { + scrollBbarExists = currentScrollBbarExists; + } + if (currentScrollBbarExists != scrollBbarExists) { + scrollBbarExists = currentScrollBbarExists; + return; + } objResizes.forEach((obj) => { const id = stringToNumber(obj.target.id); if (mode != EDITING && !changedMonitors.includes(id)) { @@ -794,6 +810,10 @@ function initPage() { $j('[id ^= "liveStream"]').each(function() { observer.observe(this); }); + + //You can immediately call startMonitors() here, but in this case the height of the monitor will initially be minimal, and then become normal, but this is not pretty. + //Check if the monitor arrangement is complete + waitingMonitorsPlaced('startMonitors'); } // end initPage function formSubmit(form) { @@ -828,8 +848,8 @@ function initGridStack(grid=null) { // When loading, we leave all monitors (according to the filters), and not just those that were saved! } else { objGridStack = GridStack.init({...opts}); + objGridStack.compact('list', true); //When reading a saved custom Layout, the monitors are not always positioned as before saving. The problem is in GridStack. Let's leave the option only for preset layout. Without this option, there may be problems with sorting monitors. } - objGridStack.compact('list', true); addEvents(objGridStack); }; @@ -838,12 +858,12 @@ function addEvents(grid, id) { //let g = (id !== undefined ? 'grid' + id + ' ' : ''); grid.on('change', function(event, items) { /* Occurs when widgets change their position/size due to constrain or direct changes */ - items.forEach(function(item) { - const currentMonitorId = stringToNumber(item.id); //We received the ID of the monitor whose size was changed - //setTriggerChangedMonitors(currentMonitorId); - //monitorsSetScale(currentMonitorId); - setTriggerChangedMonitors(currentMonitorId); - }); + //items.forEach(function(item) { + // const currentMonitorId = stringToNumber(item.id); //We received the ID of the monitor whose size was changed + // //setTriggerChangedMonitors(currentMonitorId); + // //monitorsSetScale(currentMonitorId); + // setTriggerChangedMonitors(currentMonitorId); + //}); elementResize(); }) @@ -920,76 +940,22 @@ function addEvents(grid, id) { }); } -/* -param = param['obj'] : DOM object -param = param['id'] : monitor id -*/ -function panZoomAction(action, param) { - if (action == "enable") { //Enable all object - const i = stringToNumber($j(param['obj']).children('[id ^= "liveStream"]')[0].id); - $j('.btn-zoom-in').removeClass('hidden'); - $j('.btn-zoom-out').removeClass('hidden'); - panZoom[i] = Panzoom(param['obj'], { - minScale: 1, - step: panZoomStep, - maxScale: panZoomMaxScale, - contain: 'outside', - cursor: 'auto', - }); - //panZoom[i].pan(10, 10); - //panZoom[i].zoom(1, {animate: true}); - // Binds to shift + wheel - param['obj'].parentElement.addEventListener('wheel', function(event) { - if (!shifted) { - return; - } - panZoom[i].zoomWithWheel(event); - setTriggerChangedMonitors(i); - }); - } else if (action == "disable") { //Disable a specific object - $j('.btn-zoom-in').addClass('hidden'); - $j('.btn-zoom-out').addClass('hidden'); - panZoom[param['id']].reset(); - panZoom[param['id']].resetStyle(); - panZoom[param['id']].setOptions({disablePan: true, disableZoom: true}); - panZoom[param['id']].destroy(); - } -} - function panZoomIn(el) { - if (el.target.id) { - var id = stringToNumber(el.target.id); - } else { //There may be an element without ID inside the button - var id = stringToNumber(el.target.parentElement.id); - } - if (el.ctrlKey) { - // Double the zoom step. - panZoom[id].zoom(panZoom[id].getScale() * Math.exp(panZoomStep*2), {animate: true}); - } else { - panZoom[id].zoomIn(); - } - setTriggerChangedMonitors(id); - manageCursor(id); + zmPanZoom.zoomIn(el); } function panZoomOut(el) { - if (el.target.id) { - var id = stringToNumber(el.target.id); - } else { - var id = stringToNumber(el.target.parentElement.id); - } - if (el.ctrlKey) { - // Reset zoom - panZoom[id].zoom(1, {animate: true}); - } else { - panZoom[id].zoomOut(); - } - setTriggerChangedMonitors(id); - manageCursor(id); + zmPanZoom.zoomOut(el); +} + +function changeStreamQuality() { + const streamQuality = $j('#streamQuality').val(); + setCookie('zmStreamQuality', streamQuality); + monitorsSetScale(); } function monitorsSetScale(id=null) { - //This function will probably need to be moved to the main JS file, because now used on Watch & Montage pages + // This function will probably need to be moved to the main JS file, because now used on Watch & Montage pages if (id || typeof monitorStream !== 'undefined') { //monitorStream used on Watch page. if (typeof monitorStream !== 'undefined') { @@ -1000,22 +966,33 @@ function monitorsSetScale(id=null) { }); } const el = document.getElementById('liveStream'+id); - if (panZoomEnabled) { - var panZoomScale = panZoom[id].getScale(); - } else { - var panZoomScale = 1; - } - currentMonitor.setScale(0, el.clientWidth * panZoomScale + 'px', el.clientHeight * panZoomScale + 'px', {resizeImg: false}); + const panZoomScale = panZoomEnabled ? zmPanZoom.panZoom[id].getScale() : 1; + currentMonitor.setScale(0, el.clientWidth * panZoomScale + 'px', el.clientHeight * panZoomScale + 'px', {resizeImg: false, streamQuality: $j('#streamQuality').val()}); } else { for ( let i = 0, length = monitors.length; i < length; i++ ) { const id = monitors[i].id; const el = document.getElementById('liveStream'+id); - if (panZoomEnabled) { - var panZoomScale = panZoom[id].getScale(); - } else { - var panZoomScale = 1; - } - monitors[i].setScale(0, parseInt(el.clientWidth * panZoomScale) + 'px', parseInt(el.clientHeight * panZoomScale) + 'px', {resizeImg: false}); + const panZoomScale = panZoomEnabled ? zmPanZoom.panZoom[id].getScale() : 1; + monitors[i].setScale(0, parseInt(el.clientWidth * panZoomScale) + 'px', parseInt(el.clientHeight * panZoomScale) + 'px', {resizeImg: false, streamQuality: $j('#streamQuality').val()}); + } + } +} + +function changeMonitorRate() { + const rate = $j('#changeRate').val(); + monitorsSetRate(rate); + setCookie('zmMontageRate', rate); +} + +function monitorsSetRate(fps, id=null) { + if (id) { + var currentMonitor = monitors.find((o) => { + return parseInt(o["id"]) === id; + }); + currentMonitor.setMaxFPS(fps); + } else { + for ( let i = 0, length = monitors.length; i < length; i++ ) { + monitors[i].setMaxFPS(fps); } } } @@ -1038,6 +1015,77 @@ function setTriggerChangedMonitors(id=null) { } } +function checkEndMonitorsPlaced() { + for (let i = 0, length = monitors.length; i < length; i++) { + const id = monitors[i].id; + + if (!movableMonitorData[id].stop) { + //Monitor is still moving + const objWidth = document.getElementById('liveStream'+monitors[i].id).clientWidth; + if (objWidth == movableMonitorData[id].width && objWidth !=0 ) { + movableMonitorData[id].stop = true; //The size does not change, which means it’s already in its place! + } else { + movableMonitorData[id].width = objWidth; + } + } + } + //Check if all monitors are in their places + for (let i = 0, length = movableMonitorData.length; i < length; i++) { + var monitorsEndMoving = true; + + if (movableMonitorData[i]) { //There may be empty elements + if (!movableMonitorData[i].stop) { + //Monitor is still moving + monitorsEndMoving = false; + return; + } + } + } + if (monitorsEndMoving) { + for (let i = 0, length = monitors.length; i < length; i++) { + //Clean for later use + movableMonitorData[monitors[i].id] = {'width': 0, 'stop': false}; + } + } + return monitorsEndMoving; +} + +function waitingMonitorsPlaced(action = null) { + const intervalWait = setInterval(() => { + if (checkEndMonitorsPlaced()) { + // This code may not be executed, because when opening the page we still end up in "action == 'changeRatio'" + //if (isPresetLayout(getCurrentNameLayout())) { + // objGridStack.compact('list', true); + //} + if (action == 'startMonitors') { + startMonitors(); + } else if (action == 'changeRatio') { + if (!isPresetLayout(getCurrentNameLayout())) { + return; + } + if (objGridStack) { + objGridStack.destroy(false); + } + + for (let i = 0, length = monitors.length; i < length; i++) { + const monitor = monitors[i]; + // Need to clear the current positioning "X". Otherwise, the order of the monitors will be disrupted + const monitor_frame = $j('#monitor'+monitor.id); + if (!monitor_frame) { + console.log('Error finding frame for ' + monitor.id); + continue; + } + //monitor_wrapper + monitor_frame.closest('[gs-id="' + monitor.id + '"]').removeAttr('gs-x'); + } + initGridStack(); + // You could use "objGridStack.compact('list', true)" instead of all this code, but that would mess up the monitor sorting. Because The "compact" algorithm in GridStack is not perfect. + } + clearInterval(intervalWait); + } + }, 100); +} + function changeMonitorStatusPositon() { const monitorStatusPositon = $j('#monitorStatusPositon').val(); $j('.monitorStatus').each(function updateStatusPosition() { @@ -1062,6 +1110,27 @@ function changeMonitorStatusPositon() { // Kick everything off $j(window).on('load', () => initPage()); +document.onvisibilitychange = () => { + if (document.visibilityState === "hidden") { + TimerHideShow = clearTimeout(TimerHideShow); + TimerHideShow = setTimeout(function() { + //Stop monitors when closing or hiding page + for (let i = 0, length = monitors.length; i < length; i++) { + monitors[i].kill(); + } + }, 15*1000); + } else { + TimerHideShow = clearTimeout(TimerHideShow); + //Start monitors when show page + for (let i = 0, length = monitors.length; i < length; i++) { + if (!monitors[i].started) { + monitors[i].start(); + } + } + } +}; + + /* window.onbeforeunload = function(e) { console.log('unload'); diff --git a/web/skins/classic/views/js/montage.js.php b/web/skins/classic/views/js/montage.js.php index 4131aa5219..7b1506fbd5 100644 --- a/web/skins/classic/views/js/montage.js.php +++ b/web/skins/classic/views/js/montage.js.php @@ -13,6 +13,9 @@ var monitorData = new Array(); @@ -47,6 +50,6 @@ "Positions":Positions())?$layout->Positions():'{}' ?>}; diff --git a/web/skins/classic/views/js/montagereview.js b/web/skins/classic/views/js/montagereview.js index f0cc392421..64957cc83c 100644 --- a/web/skins/classic/views/js/montagereview.js +++ b/web/skins/classic/views/js/montagereview.js @@ -25,7 +25,7 @@ function evaluateLoadTimes() { avgFrac += freeTimeLastIntervals[i]; } avgFrac = avgFrac / imageLoadTimesEvaluated; - // The larger this is(positive) the faster we can go + // The larger this is (positive) the faster we can go if (avgFrac >= 0.9) currentDisplayInterval = (currentDisplayInterval * 0.50).toFixed(1); // we can go much faster else if (avgFrac >= 0.8) currentDisplayInterval = (currentDisplayInterval * 0.55).toFixed(1); else if (avgFrac >= 0.7) currentDisplayInterval = (currentDisplayInterval * 0.60).toFixed(1); @@ -314,7 +314,7 @@ function getImageSource(monId, time) { const storage = Storage[e.StorageId] ? Storage[e.StorageId] : Storage[0]; // monitorServerId may be 0, which gives us the default Server entry const server = storage.ServerId ? Servers[storage.ServerId] : Servers[monitorServerId[monId]]; - return server.PathToZMS + '?mode=jpeg&event=' + Frame.EventId + '&frame='+frame_id + + return server.PathToZMS + '?mode=jpeg&frames=1&event=' + Frame.EventId + '&frame='+frame_id + //"&width=" + monitorCanvasObj[monId].width + //"&height=" + monitorCanvasObj[monId].height + "&scale=" + scale + @@ -794,6 +794,7 @@ function setSpeed(speed_index) { currentSpeed = parseFloat(speeds[speed_index]); speedIndex = speed_index; playSecsPerInterval = Math.floor( 1000 * currentSpeed * currentDisplayInterval ) / 1000000; + setCookie('speed', speedIndex); showSpeed(speed_index); timerFire(); } @@ -1173,7 +1174,7 @@ function load_Frames(zm_events) { $j.ajax(url+query+'.json?'+auth_relay, { timeout: 0, success: function(data) { - if (data.frames && data.frames.length) { + if (data && data.frames && data.frames.length) { zm_event.FramesById = []; let last_frame = null; diff --git a/web/skins/classic/views/js/montagereview.js.php b/web/skins/classic/views/js/montagereview.js.php index 4b93715c45..d96330ed0e 100644 --- a/web/skins/classic/views/js/montagereview.js.php +++ b/web/skins/classic/views/js/montagereview.js.php @@ -192,7 +192,7 @@ if ( isset($defaultCurrentTimeSecs) ) echo 'var currentTimeSecs=parseInt('.$defaultCurrentTimeSecs.");\n"; else - echo 'var currentTimeSecs=parseInt('.(($minTimeSecs + $maxTimeSecs)/2).");\n"; + echo 'var currentTimeSecs=parseInt('.$minTimeSecs.");\n"; echo 'var speeds=['; for ( $i=0; $i < count($speeds); $i++ ) diff --git a/web/skins/classic/views/js/watch.js b/web/skins/classic/views/js/watch.js index e2e20bfb3e..3141b4c25b 100644 --- a/web/skins/classic/views/js/watch.js +++ b/web/skins/classic/views/js/watch.js @@ -8,6 +8,8 @@ var sidebarControls = $j('#ptzControls'); var wrapperMonitor = $j('#wrapperMonitor'); var filterQuery = '&filter[Query][terms][0][attr]=MonitorId&filter[Query][terms][0][op]=%3d&filter[Query][terms][0][val]='+monitorId; var idle = 0; +var monitorStream = false; /* Stream is not started */ +var currentMonitor; var classSidebarL = 'col-sm-3'; /* id="sidebar" */ var classSidebarR = 'col-sm-2'; /* id="ptzControls" */ @@ -23,15 +25,8 @@ var coordinateMouse = { shiftMouseForTrigger_x: null, shiftMouseForTrigger_y: null }; var leftBtnStatus = {Down: false, UpAfterDown: false}; - -var panZoomEnabled = true; //Add it to settings in the future -var panZoomMaxScale = 10; -var panZoomStep = 0.3; -var panZoom = []; -var shifted; -var ctrled; - var updateScale = false; //Scale needs to be updated +var TimerHideShow; /* This is the format of the json object sent by bootstrap-table @@ -139,52 +134,18 @@ function showPtzControls() { showMode = 'control'; } -function changeSize() { - var width = $j('#width').val(); - var height = $j('#height').val(); - - //monitorStream.setScale('0', width, height); - monitorsSetScale(monitorId); - //$j('#scale').val('0'); - $j('#sidebar ul').height($j('#wrapperMonitor').height()-$j('#cycleButtons').height()); - - //setCookie('zmWatchScale', '0'); - setCookie('zmWatchWidth', width); - setCookie('zmWatchHeight', height); -} // end function changeSize() - function changeScale() { const scale = $j('#scale').val(); setCookie('zmWatchScaleNew'+monitorId, scale); setCookie('zmCycleScale', scale); monitorsSetScale(monitorId); -/* - const scale = $j('#scale').val(); - setCookie('zmWatchScale'+monitorId, scale); - $j('#width').val('auto'); - $j('#height').val('auto'); - setCookie('zmCycleScale', scale); - setCookie('zmWatchWidth', 'auto'); - setCookie('zmWatchHeight', 'auto'); - - setScale(); -*/ } -// Implement current scale, as opposed to changing it -function setScale() { -/* - const scale = $j('#scale').val(); - //monitorStream.setScale(scale, $j('#width').val(), $j('#height').val()); + +function changeStreamQuality() { + const streamQuality = $j('#streamQuality').val(); + setCookie('zmStreamQuality', streamQuality); monitorsSetScale(monitorId); - // Always turn it off, we will re-add it below. I don't know if you can add a callback multiple - // times and what the consequences would be - $j(window).off('resize', endOfResize); //remove resize handler when Scale to Fit is not active - if (scale == '0') { - $j(window).on('resize', endOfResize); //remove resize handler when Scale to Fit is not active - changeSize(); - } -*/ -} // end function changeScale +} function getStreamCmdResponse(respObj, respText) { watchdogOk('stream'); @@ -335,10 +296,14 @@ function streamCmdPause(action) { } function onPlay() { - setButtonState('pauseBtn', 'inactive'); - setButtonState('playBtn', 'active'); + //monitorStream.setup_onplay(onPlay); //IgorA100 Added for testing, but probably not required + //setButtonState('pauseBtn', 'inactive'); + //setButtonState('playBtn', 'active'); + setButtonStateWatch('pauseBtn', 'inactive'); + setButtonStateWatch('stopBtn', 'inactive'); + setButtonStateWatch('playBtn', 'unavail'); if (monitorStream.status.delayed == true) { - setButtonState('stopBtn', 'inactive'); + //setButtonState('stopBtn', 'inactive'); if (monitorStreamReplayBuffer) { setButtonState('fastFwdBtn', 'inactive'); setButtonState('slowFwdBtn', 'inactive'); @@ -346,7 +311,7 @@ function onPlay() { setButtonState('fastRevBtn', 'inactive'); } } else { - setButtonState('stopBtn', 'unavail'); + //setButtonState('stopBtn', 'unavail'); if (monitorStreamReplayBuffer) { setButtonState('fastFwdBtn', 'unavail'); setButtonState('slowFwdBtn', 'unavail'); @@ -359,14 +324,21 @@ function onPlay() { function streamCmdPlay(action) { onPlay(); if (action) { - monitorStream.streamCommand(CMD_PLAY); + if (monitorStream.started) { + //Stream was on pause + monitorStream.streamCommand(CMD_PLAY); + } else { + //Stream has been stopped + monitorStream.start(); + } } } function streamCmdStop(action) { - setButtonState('pauseBtn', 'inactive'); - setButtonState('playBtn', 'unavail'); - setButtonState('stopBtn', 'active'); + monitorStream.onplay = false; //Without this line, "onPlay" is triggered immediately due to "if (this.onplay) this.onplay();" in MonitorStream.js + //setButtonState('pauseBtn', 'inactive'); + //setButtonState('playBtn', 'unavail'); + //setButtonState('stopBtn', 'active'); if (monitorStreamReplayBuffer) { setButtonState('fastFwdBtn', 'unavail'); setButtonState('slowFwdBtn', 'unavail'); @@ -374,10 +346,14 @@ function streamCmdStop(action) { setButtonState('fastRevBtn', 'unavail'); } if (action) { - monitorStream.streamCommand(CMD_STOP); + //monitorStream.streamCommand(CMD_STOP); + monitorStream.kill(); } - setButtonState('stopBtn', 'unavail'); - setButtonState('playBtn', 'active'); + //setButtonState('stopBtn', 'unavail'); + //setButtonState('playBtn', 'active'); + setButtonStateWatch('playBtn', 'inactive'); + setButtonStateWatch('stopBtn', 'unavail'); + setButtonStateWatch('pauseBtn', 'unavail'); } function streamCmdFastFwd(action) { @@ -620,13 +596,25 @@ function fetchImage(streamImage) { } function handleClick(event) { + const targetId = event.target.id; + if (targetId.indexOf("nav-link") >= 0) { //Navigation through monitors + cycleStop(event.target); + const oldId = stringToNumber(document.querySelector('[id ^= "liveStream"]').id); + const newId = stringToNumber(targetId); + streamReStart(oldId, newId); + } else if (event.target.closest('#dvrControls')) { //Controls DVR + cyclePause(); + } else if (!event.target.closest('#wrapperMonitor')) { + return; + } + if (panZoomEnabled) { - event.preventDefault(); - if (event.target.id) { + //event.preventDefault(); //We are looking for an object with an ID, because there may be another element in the button. - var obj = event.target; - } else { - var obj = event.target.parentElement; + const obj = targetId ? event.target : event.target.parentElement; + if (!obj) { + console.log("No obj found", targetId, event.target, event.target.parentElement); + return; } if (obj.className.includes('btn-zoom-out') || obj.className.includes('btn-zoom-in')) return; @@ -639,17 +627,11 @@ function handleClick(event) { } } - if (obj.getAttribute('id').indexOf("liveStream") >= 0) { - if (ctrled && shifted) { - return; - } else if (ctrled) { - panZoom[monitorId].zoom(1, {animate: true}); - } else if (shifted) { - const scale = panZoom[monitorId].getScale() * Math.exp(panZoomStep); - const point = {clientX: event.clientX, clientY: event.clientY}; - panZoom[monitorId].zoomToPoint(scale, point, {focal: {x: event.clientX, y: event.clientY}}); - } - updateScale = true; + const obj_id = obj.getAttribute('id'); + if (obj_id) { + if (obj_id.indexOf("liveStream") >= 0) zmPanZoom.click(monitorId); + } else { + console.log("obj does not have an id", obj); } } else { // +++ Old ZoomPan algorithm. @@ -918,131 +900,7 @@ function controlSetClicked() { } } -function streamStart() { - monitorStream = new MonitorStream(monitorData[monIdx]); - monitorStream.setBottomElement(document.getElementById('dvrControls')); - - // Start the fps and status updates. give a random delay so that we don't assault the server - //monitorStream.setScale($j('#scale').val(), $j('#width').val(), $j('#height').val()); - monitorsSetScale(monitorId); - monitorStream.start(); - if (streamMode == 'single') { - monitorStream.setup_onclick(fetchImage); - } else { - monitorStream.setup_onclick(handleClick); - monitorStream.setup_onmove(handleMove); - } - monitorStream.setup_onpause(onPause); - monitorStream.setup_onplay(onPlay); - monitorStream.setup_onalarm(refresh_events_table); - - monitorStream.setButton('enableAlarmButton', enableAlmBtn); - monitorStream.setButton('forceAlarmButton', forceAlmBtn); - monitorStream.setButton('zoomOutButton', $j('zoomOutBtn')); - if (canEdit.Monitors) { - // Will be enabled by streamStatus ajax - enableAlmBtn.on('click', cmdAlarm); - forceAlmBtn.on('click', cmdForce); - } else { - forceAlmBtn.prop('title', forceAlmBtn.prop('title') + ': disabled because cannot edit Monitors'); - enableAlmBtn.prop('title', enableAlmBtn.prop('title') + ': disabled because cannot edit Monitors'); - } - - /* - if (streamMode == 'single') { - statusCmdTimer = setTimeout(statusCmdQuery, 200); - setInterval(watchdogCheck, statusRefreshTimeout*2, 'status'); - } else { - streamCmdTimer = setTimeout(streamCmdQuery, 200); - setInterval(watchdogCheck, statusRefreshTimeout*2, 'stream'); - } - if (canStream || (streamMode == 'single')) { - var streamImg = $j('#imageFeed img'); - if (!streamImg) streamImg = $j('#imageFeed object'); - if (!streamImg) { - console.error('No streamImg found for imageFeed'); - } else { - if (streamMode == 'single') { - streamImg.click(streamImg, fetchImage); - setInterval(fetchImage, imageRefreshTimeout, $j('#imageFeed img')); - } else { - streamImg.click(function(event) { - handleClick(event); - }); - streamImg.on("error", function(thing) { - console.log("Error loading image"); - console.log(thing); - setInterval(fetchImage, 100, $j('#imageFeed img')); - }); - } - } // end if have streamImg - } // streamMode native or single - */ -} - -/* -* Id - Monitor ID -* The function will probably be moved to the main JS file -*/ -function manageCursor(Id) { - const obj = document.getElementById('liveStream'+Id); - const currentScale = panZoom[Id].getScale().toFixed(1); - - if (shifted && ctrled) { - obj.closest('.zoompan').style['cursor'] = 'not-allowed'; - } else if (shifted) { - obj.closest('.zoompan').style['cursor'] = 'zoom-in'; - } else if (ctrled) { - if (currentScale == 1.0) { - obj.closest('.zoompan').style['cursor'] = 'auto'; - } else { - obj.closest('.zoompan').style['cursor'] = 'zoom-out'; - } - } else { - if (currentScale == 1.0) { - obj.closest('.zoompan').style['cursor'] = 'auto'; - } else { - obj.closest('.zoompan').style['cursor'] = 'move'; - } - } -} - -function initPage() { -// +++ Support of old ZoomPan algorithm - var useOldZoomPan = getCookie('zmUseOldZoomPan'); - const btnZoomOutBtn = document.getElementById('zoomOutBtn'); //Zoom out button below Frame. She may not - if (useOldZoomPan) { - panZoomEnabled = false; - if (btnZoomOutBtn) { - btnZoomOutBtn.classList.remove("hidden"); - } - } else { - if (btnZoomOutBtn) { - btnZoomOutBtn.classList.add("hidden"); - } - } - $j("#use-old-zoom-pan").click(function() { - useOldZoomPan = this.checked; - setCookie('zmUseOldZoomPan', this.checked); - location.reload(); - }); - document.getElementById('use-old-zoom-pan').checked = useOldZoomPan; - // --- Support of old ZoomPan algorithm - - if (panZoomEnabled) { - $j(document).on('keyup keydown', function(e) { - shifted = e.shiftKey ? e.shiftKey : e.shift; - ctrled = e.ctrlKey; - manageCursor(monitorId); - }); - $j('.zoompan').each( function() { - panZoomAction('enable', {obj: this}); - this.addEventListener('mousemove', function(e) { - //Temporarily not use - }); - }); - } - +function streamPrepareStart(monitor=null) { if (canView.Control) { // Load the settings modal into the DOM if (monitorType == 'Local') getSettingsModal(); @@ -1059,7 +917,7 @@ function initPage() { } if ((monitorType != 'WebSite') && monitorData.length) { - streamStart(); + streamStart(monitor); if (window.history.length == 1) { $j('#closeControl').html(''); } @@ -1100,6 +958,174 @@ function initPage() { setInterval(reloadWebSite, monitorRefresh*1000); } + // Manage the generate Edit button + bindButton('#editBtn', 'click', null, function onEditClick(evt) { + evt.preventDefault(); + window.location.assign("?view=monitor&mid="+monitorId); + }); + + const el = document.querySelector('.imageFeed'); + el.addEventListener('mouseenter', handleMouseEnter); + el.addEventListener('mouseleave', handleMouseLeave); + + const i = setInterval(function() { + if (document.querySelector('[id ^= "liveStream"]').offsetHeight > 20) { + //You need to wait until the image appears. + clearInterval(i); + document.getElementById('monitor').classList.remove('hidden-shift'); + monitorsSetScale(monitorId); + } + }, 100); + setButtonStateWatch('stopBtn', 'active'); + setTimeout(dataOnClick, 100); +} + +function handleMouseEnter(event) { + //Displaying "Scale" and other buttons at the top of the monitor image + const id = stringToNumber(this.id); + $j('#button_zoom' + id).stop(true, true).slideDown('fast'); +} + +function handleMouseLeave(event) { + const id = stringToNumber(this.id); + $j('#button_zoom' + id).stop(true, true).slideUp('fast'); +} + +function streamStart(monitor = null) { + if (monitor) { + monitorStream = new MonitorStream(monitor); + } else { + monitorStream = new MonitorStream(monitorData[monIdx]); + } + monitorStream.setBottomElement(document.getElementById('dvrControls')); + // Start the fps and status updates. give a random delay so that we don't assault the server + //monitorStream.setScale($j('#scale').val(), $j('#width').val(), $j('#height').val()); + //monitorsSetScale(monitorId); + monitorStream.start(); + if (streamMode == 'single') { + monitorStream.setup_onclick(fetchImage); + } else { + monitorStream.setup_onclick(handleClick); + monitorStream.setup_onmove(handleMove); + } + monitorStream.setup_onpause(onPause); + monitorStream.setup_onplay(onPlay); + monitorStream.setup_onalarm(refresh_events_table); + + monitorStream.setButton('enableAlarmButton', enableAlmBtn); + monitorStream.setButton('forceAlarmButton', forceAlmBtn); + monitorStream.setButton('zoomOutButton', $j('zoomOutBtn')); + if (canEdit.Monitors) { + // Will be enabled by streamStatus ajax + enableAlmBtn.on('click', cmdAlarm); + forceAlmBtn.on('click', cmdForce); + } else { + forceAlmBtn.prop('title', forceAlmBtn.prop('title') + ': disabled because cannot edit Monitors'); + enableAlmBtn.prop('title', enableAlmBtn.prop('title') + ': disabled because cannot edit Monitors'); + } +} + +function streamReStart(oldId, newId) { + document.getElementById('monitor').classList.add('hidden-shift'); + const el = document.querySelector('.imageFeed'); + const newMonitorName = document.getElementById('nav-item-cycle'+newId).querySelector('a').textContent; + currentMonitor = monitorData.find((o) => { + return parseInt(o["id"]) === newId; + }); + const url = new URL(document.location.href); + monitorId = newId; + filterQuery = '&filter[Query][terms][0][attr]=MonitorId&filter[Query][terms][0][op]=%3d&filter[Query][terms][0][val]='+monitorId; + document.querySelector('title').textContent = newMonitorName; + url.searchParams.set('mid', monitorId); + history.pushState(null, "", url); + + zmPanZoom.action('disable', {id: oldId}); + if (monitorStream) { + monitorStream.kill(); + } + el.removeEventListener('mouseenter', handleMouseEnter); + el.removeEventListener('mouseleave', handleMouseLeave); + + //Change main monitor block + document.getElementById('monitor').innerHTML = currentMonitor.streamHTML; + + //Change active element of the navigation menu + document.getElementById('nav-item-cycle'+oldId).querySelector('a').classList.remove("active"); + document.getElementById('nav-item-cycle'+newId).querySelector('a').classList.add("active"); + + //Set global variables from the current monitor + monitorWidth = currentMonitor.monitorWidth; + monitorHeight = currentMonitor.monitorHeight; + monitorType = currentMonitor.monitorType; + monitorRefresh = currentMonitor.monitorRefresh; + monitorStreamReplayBuffer = currentMonitor.monitorStreamReplayBuffer; + monitorControllable = currentMonitor.monitorControllable; + streamMode = currentMonitor.streamMode; + + table.bootstrapTable('destroy'); + applyMonitorControllable(); + streamPrepareStart(currentMonitor); + zmPanZoom.init(); + zmPanZoom.init({objString: '.imageFeed', disablePan: true, contain: 'inside', additional: true}); + loadFontFaceObserver(); + //document.getElementById('monitor').classList.remove('hidden-shift'); +} + +function applyMonitorControllable() { + const ptzToggle = document.getElementById('ptzToggle'); + if (!ptzToggle) { + console.log('ptz toggle is not present. Likely OPT_CONTROL is off'); + return; + } + if (currentMonitor.monitorControllable) { + const ptzShow = getCookie('ptzShow'); + + ptzToggle.classList.remove("disabled"); + ptzToggle.disabled=false; + sidebarControls.html(currentMonitor.ptzControls); + if (ptzShow) { + sidebarControls.show(); + ptzToggle.classList.remove("btn-secondary"); + ptzToggle.classList.add("btn-primary"); + } else { + sidebarControls.hide(); + ptzToggle.classList.remove("btn-primary"); + ptzToggle.classList.add("btn-secondary"); + } + } else { + ptzToggle.classList.add("disabled"); + ptzToggle.disabled=true; + sidebarControls.html(''); + sidebarControls.hide(); + } + changeObjectClass(); +} + +function initPage() { +// +++ Support of old ZoomPan algorithm + var useOldZoomPan = getCookie('zmUseOldZoomPan'); + const btnZoomOutBtn = document.getElementById('zoomOutBtn'); //Zoom out button below Frame. She may not + if (useOldZoomPan) { + panZoomEnabled = false; + if (btnZoomOutBtn) { + btnZoomOutBtn.classList.remove("hidden"); + } + } else { + if (btnZoomOutBtn) { + btnZoomOutBtn.classList.add("hidden"); + } + } + $j("#use-old-zoom-pan").click(function() { + useOldZoomPan = this.checked; + setCookie('zmUseOldZoomPan', this.checked); + location.reload(); + }); + document.getElementById('use-old-zoom-pan').checked = useOldZoomPan; + // --- Support of old ZoomPan algorithm + + zmPanZoom.init(); + zmPanZoom.init({objString: '.imageFeed', disablePan: true, contain: 'inside', additional: true}); + // Manage the BACK button bindButton('#backBtn', 'click', null, function onBackClick(evt) { evt.preventDefault(); @@ -1121,12 +1147,6 @@ function initPage() { $j('#settingsModal').modal('show'); }); - // Manage the generate Edit button - bindButton('#editBtn', 'click', null, function onEditClick(evt) { - evt.preventDefault(); - window.location.assign("?view=monitor&mid="+monitorId); - }); - bindButton('#cyclePlayBtn', 'click', null, cycleStart); bindButton('#cyclePauseBtn', 'click', null, cyclePause); bindButton('#cycleNextBtn', 'click', null, cycleNext); @@ -1167,17 +1187,6 @@ function initPage() { } }, 10*1000); } - $j(".imageFeed").hover( - //Displaying "Scale" and other buttons at the top of the monitor image - function() { - const id = stringToNumber(this.id); - $j('#button_zoom' + id).stop(true, true).slideDown('fast'); - }, - function() { - const id = stringToNumber(this.id); - $j('#button_zoom' + id).stop(true, true).slideUp('fast'); - } - ); setInterval(() => { //Updating Scale. When quickly scrolling the mouse wheel or quickly pressing Zoom In/Out, you should not set Scale very often. @@ -1185,10 +1194,22 @@ function initPage() { monitorsSetScale(monitorId); updateScale = false; } - }, 500); + }, 300); + document.addEventListener('click', function(event) { + handleClick(event); + }); + + //document.getElementById('monitor').classList.remove('hidden-shift'); changeObjectClass(); - changeSize(); + + currentMonitor = monitorData.find((o) => { + return parseInt(o["id"]) === monitorId; + }); + if (currentMonitor) { + applyMonitorControllable(); + } + streamPrepareStart(); } // initPage function watchFullscreen() { @@ -1206,7 +1227,7 @@ function watchFullscreen() { } function watchAllEvents() { - window.location.replace(document.getElementById('allEventsBtn').getAttribute('data-url')); + window.location.replace(currentMonitor.urlForAllEvents); } var intervalId; @@ -1228,13 +1249,20 @@ function cyclePause() { } function cycleStart() { - secondsToCycle = $j('#cyclePeriod').val(); + if (secondsToCycle == 0) secondsToCycle = $j('#cyclePeriod').val(); intervalId = setInterval(nextCycleView, 1000); cycle = true; $j('#cyclePauseBtn').show(); $j('#cyclePlayBtn').hide(); } +function cycleStop(target) { + secondsToCycle = 0; + monIdx = target.getAttribute('data-monIdx'); + $j('#secondsToCycle').text(''); + cyclePause(); +} + function cycleNext() { monIdx ++; if (monIdx >= monitorData.length) { @@ -1245,7 +1273,19 @@ function cycleNext() { } clearInterval(intervalId); monitorStream.kill(); - window.location.replace('?view=watch&cycle='+cycle+'&mid='+monitorData[monIdx].id+'&mode='+mode); + + // +++ Start next monitor + let oldId; + if (monIdx == 0) { + oldId = monitorData[monitorData.length-1].id; + } else { + oldId = monitorData[monIdx-1].id; + } + const newId = monitorData[monIdx].id; + streamReStart(oldId, newId); + cycleStart(); + // --- Start next monitor + //window.location.replace('?view=watch&cycle='+cycle+'&mid='+monitorData[monIdx].id+'&mode='+mode); } function cyclePrev() { @@ -1257,8 +1297,21 @@ function cyclePrev() { console.log('No monitorData for ' + monIdx); } clearInterval(intervalId); - monitorStream.stop(); - window.location.replace('?view=watch&cycle='+cycle+'&mid='+monitorData[monIdx].id+'&mode='+mode); + //monitorStream.stop(); + monitorStream.kill(); + + // +++ Start previous monitor + let oldId; + if (monIdx == monitorData.length - 1) { + oldId = monitorData[0].id; + } else { + oldId = monitorData[monIdx+1].id; + } + const newId = monitorData[monIdx].id; + streamReStart(oldId, newId); + cycleStart(); + // --- Start previous monitors + //window.location.replace('?view=watch&cycle='+cycle+'&mid='+monitorData[monIdx].id+'&mode='+mode); } function cyclePeriodChange() { @@ -1277,7 +1330,7 @@ function cycleToggle(e) { button.toggleClass('btn-secondary'); button.toggleClass('btn-primary'); changeObjectClass(); - changeSize(); + monitorsSetScale(monitorId); } function ptzToggle(e) { @@ -1292,7 +1345,7 @@ function ptzToggle(e) { button.toggleClass('btn-secondary'); button.toggleClass('btn-primary'); changeObjectClass(); - changeSize(); + monitorsSetScale(monitorId); } function changeRate(e) { @@ -1346,128 +1399,125 @@ function changeObjectClass() { } } -/* -param = param['obj'] : DOM object -param = param['id'] : monitor id -*/ -function panZoomAction(action, param) { - if (action == "enable") { - //Enable all object - const i = stringToNumber($j(param['obj']).children('[id ^= "liveStream"]')[0].id); - $j('.btn-zoom-in').removeClass('hidden'); - $j('.btn-zoom-out').removeClass('hidden'); - panZoom[i] = Panzoom(param['obj'], { - minScale: 1, - step: panZoomStep, - maxScale: panZoomMaxScale, - contain: 'outside', - cursor: 'auto', - }); - //panZoom[i].pan(10, 10); - //panZoom[i].zoom(1, {animate: true}); - // Binds to shift + wheel - param['obj'].parentElement.addEventListener('wheel', function(event) { - if (!shifted) { - return; - } - panZoom[i].zoomWithWheel(event); - updateScale = true; - }); - } else if (action == "disable") { - //Disable a specific object - $j('.btn-zoom-in').addClass('hidden'); - $j('.btn-zoom-out').addClass('hidden'); - panZoom[param['id']].reset(); - panZoom[param['id']].resetStyle(); - panZoom[param['id']].setOptions({disablePan: true, disableZoom: true}); - panZoom[param['id']].destroy(); - } -} - function panZoomIn(el) { - /* - if (el.target.id) { - //For Montage page - var id = stringToNumber(el.target.id); - } else { //There may be an element without ID inside the button - var id = stringToNumber(el.target.parentElement.id); - } - */ - var id = monitorId; //For Watch page - if (el.ctrlKey) { - // Double the zoom step. - panZoom[id].zoom(panZoom[id].getScale() * Math.exp(panZoomStep*2), {animate: true}); - } else { - panZoom[id].zoomIn(); - } - updateScale = true; - manageCursor(id); + zmPanZoom.zoomIn(el); } function panZoomOut(el) { + zmPanZoom.zoomOut(el); +} + +function panZoomEventPanzoomzoom(event) { + //Temporarily not in use /* - if (el.target.id) { - //For Montage page - var id = stringToNumber(el.target.id); - } else { //There may be an element without ID inside the button - var id = stringToNumber(el.target.parentElement.id); + const obj = event.target; + const parent = obj.parentNode; + const objDim = obj.getBoundingClientRect(); + const parentDim = parent.getBoundingClientRect(); + const top = objDim.top - parentDim.top; + const h = objDim.height + top; + console.log("object", obj); + console.log("parentDim:", parentDim); + console.log("objDim:", objDim); + console.log("H:", h); + if (h>30) { + parent.style.height = h+'px'; + console.log('panzoomzoom', event.detail) // => { x: 0, y: 0, scale: 1 } } */ - var id = monitorId; //For Watch page - if (el.ctrlKey) { - // Reset zoom - panZoom[id].zoom(1, {animate: true}); - } else { - panZoom[id].zoomOut(); - } - updateScale = true; - manageCursor(id); +} + +function panZoomEventPanzoomchange(event) { + } function monitorsSetScale(id=null) { //This function will probably need to be moved to the main JS file, because now used on Watch & Montage pages if (id || typeof monitorStream !== 'undefined') { - //monitorStream used on Watch page. - if (monitorStream) { + if (monitorStream !== false) { + //monitorStream used on Watch page. var curentMonitor = monitorStream; - } else { + } else if (typeof monitors !== 'undefined') { + //used on Montage, Watch & Event page. var curentMonitor = monitors.find((o) => { return parseInt(o["id"]) === id; }); + } else { + //Stream is missing + return; } //const el = document.getElementById('liveStream'+id); - if (panZoomEnabled) { - var panZoomScale = panZoom[id].getScale(); + if (panZoomEnabled && zmPanZoom.panZoom[id]) { + var panZoomScale = zmPanZoom.panZoom[id].getScale(); } else { var panZoomScale = 1; } const scale = $j('#scale').val(); let resize; + let width; + let maxWidth = ''; + let height; + let overrideHW = false; + let defScale = 0; + const landscape = curentMonitor.width / curentMonitor.height > 1 ? true : false; //Image orientation. + if (scale == '0') { //Auto, Width is calculated based on the occupied height so that the image and control buttons occupy the visible part of the screen. resize = true; + width = 'auto'; + height = 'auto'; } else if (scale == '100') { //Actual, 100% of original size resize = false; + width = curentMonitor.width + 'px'; + height = curentMonitor.height + 'px'; } else if (scale == 'fit_to_width') { //Fit to screen width resize = false; + width = parseInt(window.innerWidth * panZoomScale) + 'px'; + height = 'auto'; + } else if (scale.indexOf("px") > -1) { + if (landscape) { + maxWidth = scale; + defScale = parseInt(Math.min(stringToNumber(scale), window.innerWidth) / curentMonitor.width * panZoomScale * 100); + height = 'auto'; + } else { + defScale = parseInt(Math.min(stringToNumber(scale), window.innerHeight) / curentMonitor.height * panZoomScale * 100); + height = scale; + } + resize = true; + width = 'auto'; + overrideHW = true; } - //const resize = (parseInt($j('#scale').val()) == 0) ? true : false; if (resize) { - document.getElementById('monitor'+id).style.width = 'max-content'; //Required when switching from resize=false to resize=true - } - //curentMonitor.setScale(0, el.clientWidth * panZoomScale + 'px', el.clientHeight * panZoomScale + 'px', {resizeImg:true, scaleImg:panZoomScale}); - curentMonitor.setScale(0, 'auto', 'auto', {resizeImg: resize, scaleImg: panZoomScale}); - if (!resize) { + if (scale == '0') { + document.getElementById('monitor'+id).style.width = 'max-content'; //Required when switching from resize=false to resize=true + } + document.getElementById('monitor'+id).style.maxWidth = maxWidth; + if (!landscape) { //PORTRAIT + document.getElementById('monitor'+id).style.width = 'max-content'; + document.getElementById('liveStream'+id).style.height = height; + } + } else { document.getElementById('liveStream'+id).style.height = ''; + document.getElementById('monitor'+id).style.width = width; + document.getElementById('monitor'+id).style.maxWidth = ''; if (scale == 'fit_to_width') { document.getElementById('monitor'+id).style.width = ''; } else if (scale == '100') { + document.getElementById('liveStream'+id).style.width = width; + } + } + //curentMonitor.setScale(0, maxWidth ? maxWidth : width, height, {resizeImg: resize, scaleImg: panZoomScale}); + curentMonitor.setScale(defScale, width, height, {resizeImg: resize, scaleImg: panZoomScale, streamQuality: $j('#streamQuality').val()}); + if (overrideHW) { + if (!landscape) { //PORTRAIT document.getElementById('monitor'+id).style.width = 'max-content'; - document.getElementById('liveStream'+id).style.width = 'auto'; + } else { + document.getElementById('liveStream'+id).style.height = 'auto'; + document.getElementById('monitor'+id).style.width = 'auto'; } } } else { @@ -1482,39 +1532,76 @@ function monitorsSetScale(id=null) { const scale = $j('#scale').val(); let resize; + let width; + let height; + if (scale == '0') { //Auto, Width is calculated based on the occupied height so that the image and control buttons occupy the visible part of the screen. resize = true; + width = 'auto'; + height = 'auto'; } else if (scale == '100') { //Actual, 100% of original size resize = false; + width = monitors[i].width + 'px'; + height = monitors[i].height + 'px'; } else if (scale == 'fit_to_width') { //Fit to screen width resize = false; + width = parseInt(window.innerWidth * panZoomScale) + 'px'; + height = 'auto'; } if (resize) { document.getElementById('monitor'+id).style.width = 'max-content'; //Required when switching from resize=false to resize=true } //monitors[i].setScale(0, parseInt(el.clientWidth * panZoomScale) + 'px', parseInt(el.clientHeight * panZoomScale) + 'px', {resizeImg:true, scaleImg:panZoomScale}); - monitors[i].setScale(0, 'auto', 'auto', {resizeImg: resize, scaleImg: panZoomScale}); + monitors[i].setScale(0, width, height, {resizeImg: resize, scaleImg: panZoomScale}); if (!resize) { document.getElementById('liveStream'+id).style.height = ''; if (scale == 'fit_to_width') { document.getElementById('monitor'+id).style.width = ''; } else if (scale == '100') { document.getElementById('monitor'+id).style.width = 'max-content'; - document.getElementById('liveStream'+id).style.width = 'auto'; + document.getElementById('liveStream'+id).style.width = width; } } } } } -function stringToNumber(str) { - //This function will probably need to be moved to the main JS file, because now used on Watch & Montage pages - return parseInt(str.replace(/\D/g, '')); -} - // Kick everything off $j( window ).on("load", initPage); + +document.onvisibilitychange = () => { + if (document.visibilityState === "hidden") { + TimerHideShow = clearTimeout(TimerHideShow); + TimerHideShow = setTimeout(function() { + //Stop monitor when closing or hiding page + if (monitorStream) { + monitorStream.kill(); + } + }, 15*1000); + } else { + //Start monitor when show page + if (monitorStream && !monitorStream.started) { + monitorStream.start(); + } + } +}; + +function setButtonStateWatch(element_id, btnClass) { + //Temporary function so as not to break anything else, because analysis of the setButtonState function in skin.js is required, + //and also review the logic of the buttons and more (if (this.onplay) this.onplay() in MonitorStream.js) var element = document.getElementById(element_id); + var element = document.getElementById(element_id); + if ( element ) { + element.className = btnClass; + if (btnClass == 'unavail') { + element.disabled = true; + } else { + element.disabled = false; + } + } else { + console.log('Element was null or not found in setButtonState. id:'+element_id); + } +} diff --git a/web/skins/classic/views/js/watch.js.php b/web/skins/classic/views/js/watch.js.php index 8142cf0669..cdb9b289b3 100644 --- a/web/skins/classic/views/js/watch.js.php +++ b/web/skins/classic/views/js/watch.js.php @@ -3,6 +3,7 @@ global $nextMid; global $options; global $monitors; + global $monitorsExtraData; global $streamMode; global $showPtzControls; global $monitor; @@ -53,7 +54,17 @@ 'onclick': function(){window.location.assign( '?view=watch&mid=Id() ?>' );}, 'type': 'Type() ?>', 'refresh': 'Refresh() ?>', - 'janus_pin': 'Janus_Pin() ?>' + 'janus_pin': 'Janus_Pin() ?>', + 'streamHTML': 'Id()]['StreamHTML']) ?>', + 'urlForAllEvents': 'Id()]['urlForAllEvents'] ?>', + 'ptzControls': 'Id()]['ptzControls']) ?>', + 'monitorWidth': parseInt('ViewWidth() ?>'), + 'monitorHeight': parseInt('ViewHeight() ?>'), + 'monitorType': 'Type() ?>', + 'monitorRefresh': 'Refresh() ?>', + 'monitorStreamReplayBuffer': parseInt('StreamReplayBuffer() ?>'), + 'monitorControllable': Controllable()?'true':'false' ?>, + 'streamMode': '' }; '; -var statusRefreshTimeout = ; -var eventsRefreshTimeout = ; -var imageRefreshTimeout = ; +const statusRefreshTimeout = ; +const eventsRefreshTimeout = ; +const imageRefreshTimeout = ; -var canStream = ; +const canStream = ; var imageControlMode = 'Control(); diff --git a/web/skins/classic/views/js/zone.js b/web/skins/classic/views/js/zone.js index dafef67c73..4c3b9f3178 100644 --- a/web/skins/classic/views/js/zone.js +++ b/web/skins/classic/views/js/zone.js @@ -7,6 +7,7 @@ var refreshBtn = $j('#refreshBtn'); var analyseBtn = $j('#analyseBtn'); var monitors = []; var analyse_frames = true; +var TimerHideShow; function validateForm( form ) { var errors = []; @@ -730,6 +731,14 @@ function initPage() { }); } // initPage +function panZoomIn(el) { + zmPanZoom.zoomIn(el); +} + +function panZoomOut(el) { + zmPanZoom.zoomOut(el); +} + function imageLoadEvent() { // We only need this event on the first image load to set dimensions. // Turn it off after it has been called. @@ -753,4 +762,23 @@ function Polygon_calcArea(coords) { return Math.round(Math.abs(float_area)); } +document.onvisibilitychange = () => { + if (document.visibilityState === "hidden") { + TimerHideShow = clearTimeout(TimerHideShow); + TimerHideShow = setTimeout(function() { + //Stop monitors when closing or hiding page + for (let i = 0, length = monitorData.length; i < length; i++) { + monitors[i].kill(); + } + }, 15*1000); + } else { + //Start monitors when show page + for (let i = 0, length = monitorData.length; i < length; i++) { + if (!monitors[i].started) { + monitors[i].start(); + } + } + } +}; + window.addEventListener('DOMContentLoaded', initPage); diff --git a/web/skins/classic/views/js/zones.js b/web/skins/classic/views/js/zones.js index 13f9d08235..0b7303cb98 100644 --- a/web/skins/classic/views/js/zones.js +++ b/web/skins/classic/views/js/zones.js @@ -5,6 +5,7 @@ function AddNewZone(el) { } var monitors = new Array(); +var TimerHideShow; function initPage() { for ( var i = 0, length = monitorData.length; i < length; i++ ) { @@ -35,6 +36,14 @@ function initPage() { }); } +function panZoomIn(el) { + zmPanZoom.zoomIn(el); +} + +function panZoomOut(el) { + zmPanZoom.zoomOut(el); +} + function streamCmdQuit() { for ( var i = 0, length = monitorData.length; i < length; i++ ) { monitors[i] = new MonitorStream(monitorData[i]); @@ -44,3 +53,21 @@ function streamCmdQuit() { window.addEventListener('DOMContentLoaded', initPage); +document.onvisibilitychange = () => { + if (document.visibilityState === "hidden") { + TimerHideShow = clearTimeout(TimerHideShow); + TimerHideShow = setTimeout(function() { + //Stop monitors when closing or hiding page + for (let i = 0, length = monitorData.length; i < length; i++) { + monitors[i].kill(); + } + }, 15*1000); + } else { + //Start monitors when show page + for (let i = 0, length = monitorData.length; i < length; i++) { + if (!monitors[i].started) { + monitors[i].start(); + } + } + } +}; diff --git a/web/skins/classic/views/monitor.php b/web/skins/classic/views/monitor.php index c62dba686c..3f38ce2a3f 100644 --- a/web/skins/classic/views/monitor.php +++ b/web/skins/classic/views/monitor.php @@ -294,6 +294,10 @@ function fourcc($a, $b, $c, $d) { '4' => translate('32BitColour') ); +$devices = [''=>translate('Other')]; +foreach (glob('/dev/video*') as $device) + $devices[$device] = $device; + $orientations = array( 'ROTATE_0' => translate('Normal'), 'ROTATE_90' => translate('RotateRight'), @@ -645,23 +649,25 @@ class="nav-link" ?>
  • - + 1 ? htmlSelect('newMonitor[Devices]', $devices, $monitor->Device()) : ''; ?> + Device() and isset($devices[$monitor->Device()]) ) ? 'style="display: none;"' : '' ?> + />
  • -
  • - - 'Video For Linux version 2', ); if (!ZM_HAS_V4L2) unset($localMethods['v4l2']); -if (!isset($localMethods[$monitor->Method()])) $monitor->Method('v4l2'); -echo htmlSelect('newMonitor[Method]', $localMethods, - ((count($localMethods)==1) ? array_keys($localMethods)[0] : $monitor->Method()), - array('data-on-change'=>'submitTab', 'data-tab-name'=>$tab) ); -?> -
  • -Method()])) $monitor->Method(array_keys($localMethods)[0]); +if (count($localMethods)>1) { + echo '
  • '; + echo htmlSelect('newMonitor[Method]', $localMethods, $monitor->Method(), ['data-on-change'=>'submitTab', 'data-tab-name'=>$tab] ); + echo '
  • '.PHP_EOL; +} else { + echo ''.PHP_EOL; +} if ( ZM_HAS_V4L2 && $monitor->Method() == 'v4l2' ) { ?>
  • @@ -1146,6 +1152,7 @@ class="nav-link" 'h264_omx' => 'h264_omx', 'h264_qsv' => 'h264_qsv', 'h264_vaapi' => 'h264_vaapi', + 'h264_v4l2m2m' => 'h264_v4l2m2m', 'libx265' => 'libx265', 'hevc_nvenc' => 'hevc_nvenc', 'hevc_qsv' => 'hevc_qsv', @@ -1176,6 +1183,10 @@ class="nav-link"
  • +
  • + + WallClockTimestamps() ) { ?> checked="checked"/> +
  • Type() == 'Ffmpeg' ) { ?> @@ -1295,6 +1306,34 @@ class="nav-link" ); echo htmlSelect('newMonitor[DefaultCodec]', $codecs, $monitor->DefaultCodec()); ?>
  • +
  • +Type()=='WebSite' or ($monitor->CaptureFPS() && $monitor->Capturing() != 'None'); + $options = array(); + + $ratio_factor = $monitor->ViewWidth() ? $monitor->ViewHeight() / $monitor->ViewWidth() : 1; + $options['width'] = ZM_WEB_LIST_THUMB_WIDTH; + $options['height'] = ZM_WEB_LIST_THUMB_HEIGHT ? ZM_WEB_LIST_THUMB_HEIGHT : ZM_WEB_LIST_THUMB_WIDTH*$ratio_factor; + $options['scale'] = $monitor->ViewWidth() ? intval(100*ZM_WEB_LIST_THUMB_WIDTH / $monitor->ViewWidth()) : 100; + $options['mode'] = 'jpeg'; + $options['frames'] = 1; + + $stillSrc = $monitor->getStreamSrc($options); + $streamSrc = $monitor->getStreamSrc(array('scale'=>$options['scale']*5)); + + $thmbWidth = ( $options['width'] ) ? 'width:'.$options['width'].'px;' : ''; + $thmbHeight = ( $options['height'] ) ? 'height:'.$options['height'].'px;' : ''; + + $imgHTML = '
    ' : '>'; + $imgHTML .= '
    '; + echo $imgHTML; +?> +
  • " echo htmlSelect('newMonitor[ControlId]', $controlTypes, $monitor->ControlId()); if ( canEdit('Control') ) { - echo ' '.makeLink('?view=controlcaps', translate('Edit')); + echo ' '.makeLink('?view=options&tab=control', translate('Edit')); } ?> diff --git a/web/skins/classic/views/montage.php b/web/skins/classic/views/montage.php index 9ef9dc97cd..37eac58b53 100644 --- a/web/skins/classic/views/montage.php +++ b/web/skins/classic/views/montage.php @@ -59,7 +59,6 @@ ); $monitorStatusPositonSelected = 'outsideImgBottom'; - if (isset($_REQUEST['monitorStatusPositonSelected'])) { $monitorStatusPositonSelected = $_REQUEST['monitorStatusPositonSelected']; } else if (isset($_COOKIE['zmMonitorStatusPositonSelected'])) { @@ -67,28 +66,53 @@ } $layouts = ZM\MontageLayout::find(NULL, array('order'=>"lower('Name')")); +// layoutsById is used in the dropdown, so needs to be sorted $layoutsById = array(); -$FreeFormLayoutId = 0; -foreach ( $layouts as $l ) { - if ( $l->Name() == 'Freeform' ) { - $FreeFormLayoutId = $l->Id(); - $layoutsById[$l->Id()] = $l; - break; - } +$presetLayoutsNames = array( //Order matters! + 'Auto', + '1 Wide', + '2 Wide', + '3 Wide', + '4 Wide', + '6 Wide', + '8 Wide', + '12 Wide', + '16 Wide' +); + +/* Create an array "Name"=>layouts to make it easier to find IDs by name */ +$layoutsByName = array(); +foreach ($layouts as $l) { + if ($l->Name() == 'Freeform') $l->Name('Auto'); + $layoutsByName[$l->Name()] = $l; } -foreach ( $layouts as $l ) { - if ( $l->Name() != 'Freeform' ) - $layoutsById[$l->Id()] = $l; + +/* Fill with preinstalled Layouts. They should always come first. + * Also sorting 1 Wide and 11 Wide fails... so need a smarter sort + */ +foreach ($presetLayoutsNames as $name) { + if (array_key_exists($name, $layoutsByName)) // Layout may be missing in DB (rare case during update process) + $layoutsById[$layoutsByName[$name]->Id()] = $layoutsByName[$name]; +} + +/* Add custom Layouts & assign objects instead of names for preset Layouts */ +foreach ($layouts as $l) { + $layoutsById[$l->Id()] = $l; } zm_session_start(); -$layout_id = ''; +$layout_id = 0; if ( isset($_COOKIE['zmMontageLayout']) ) { - $layout_id = $_SESSION['zmMontageLayout'] = $_COOKIE['zmMontageLayout']; + $layout_id = $_SESSION['zmMontageLayout'] = validCardinal($_COOKIE['zmMontageLayout']); } elseif ( isset($_SESSION['zmMontageLayout']) ) { - $layout_id = $_SESSION['zmMontageLayout']; + $layout_id = validCardinal($_SESSION['zmMontageLayout']); } +if (!$layout_id || !isset($layoutsById[$layout_id])) { + $layout_id = $layoutsByName['Auto']->Id(); +} +$layout = $layoutsById[$layout_id]; +$layout_is_preset = array_search($layout->Name(), $presetLayoutsNames) === false ? false : true; $options = array(); @@ -138,12 +162,29 @@ */ } +$streamQualitySelected = '0'; +if (isset($_REQUEST['streamQuality'])) { + $streamQualitySelected = $_REQUEST['streamQuality']; +} else if (isset($_COOKIE['zmStreamQuality'])) { + $streamQualitySelected = $_COOKIE['zmStreamQuality']; +} else if (isset($_SESSION['zmStreamQuality']) ) { + $streamQualitySelected = $_SESSION['zmStreamQuality']; +} + +if (!empty($_REQUEST['maxfps']) and validFloat($_REQUEST['maxfps']) and ($_REQUEST['maxfps']>0)) { + $options['maxfps'] = validHtmlStr($_REQUEST['maxfps']); +} else if (isset($_COOKIE['zmMontageRate'])) { + $options['maxfps'] = validHtmlStr($_COOKIE['zmMontageRate']); +} else { + $options['maxfps'] = ''; // unlimited +} + session_write_close(); -ob_start(); include('_monitor_filters.php'); -$filterbar = ob_get_contents(); -ob_end_clean(); +$resultMonitorFilters = buildMonitorsFilters(); +$filterbar = $resultMonitorFilters['filterBar']; +$displayMonitors = $resultMonitorFilters['displayMonitors']; $need_hls = false; $need_janus = false; @@ -173,34 +214,25 @@ } } # end foreach Monitor -if (!$layout_id) { - $default_layout = ''; - if (!$default_layout) { - if ((count($monitors) > 5) and (count($monitors)%5 == 0)) { - $default_layout = '5 Wide'; - } else if ((count($monitors) > 4) and (count($monitors)%4 == 0)) { - $default_layout = '4 Wide'; - } else if (count($monitors)%3 == 0) { - $default_layout = '3 Wide'; - } else { - $default_layout = '2 Wide'; - } - } - foreach ($layouts as $l) { - if ($l->Name() == $default_layout) { - $layout_id = $l->Id(); - } - } -} -$Layout = ''; -$Positions = ''; -if ( $layout_id and is_numeric($layout_id) and isset($layoutsById[$layout_id]) ) { - $Layout = $layoutsById[$layout_id]; - $Positions = json_decode($Layout->Positions(), true); +$default_layout = ''; + +$monitorCount = count($monitors); +if ($monitorCount <= 3) { + $default_layout = $monitorCount . ' Wide'; +} else if ($monitorCount <= 4) { + $default_layout = '2 Wide'; +} else if ($monitorCount <= 6) { + $default_layout = '3 Wide'; +} else if ($monitorCount%4 == 0) { + $default_layout = '4 Wide'; +} else if ($monitorCount%6 == 0) { + $default_layout = '6 Wide'; } else { - ZM\Debug('Layout not found'); + $default_layout = '4 Wide'; } +$AutoLayoutName = $default_layout; + xhtmlHeaders(__FILE__, translate('Montage')); getBodyTopHTML(); echo getNavBarHTML(); @@ -242,25 +274,44 @@
    - + 'monitorStatusPositon', 'data-on-change'=>'changeMonitorStatusPositon', 'class'=>'chosen')); ?> + + + translate('Unlimited'), + '0.10' => '1/10' .translate('FPS'), + '0.50' => '1/2' .translate('FPS'), + '1' => '1 '.translate('FPS'), + '2' => '2 '.translate('FPS'), + '5' => '5 '.translate('FPS'), + '10' => '10 '.translate('FPS'), + '20' => '20 '.translate('FPS'), +); +echo htmlSelect('changeRate', $maxfps_options, $options['maxfps'], array('id'=>'changeRate', 'data-on-change'=>'changeMonitorRate', 'class'=>'chosen')); +?> + - 'ratio', 'data-on-change'=>'changeRatioForAll', 'class'=>'chosen')); ?> + 'ratio', 'data-on-change'=>'changeRatioForAll', 'class'=>'chosen')); ?> + + + + 'changeStreamQuality','id'=>'streamQuality')); ?> -
    + diff --git a/web/skins/classic/views/montagereview.php b/web/skins/classic/views/montagereview.php index c7a6664032..8ba8df6ff6 100644 --- a/web/skins/classic/views/montagereview.php +++ b/web/skins/classic/views/montagereview.php @@ -57,10 +57,11 @@ } require_once('includes/Filter.php'); -ob_start(); include('_monitor_filters.php'); -$filter_bar = ob_get_contents(); -ob_end_clean(); +$resultMonitorFilters = buildMonitorsFilters(); +$filterbar = $resultMonitorFilters['filterBar']; +$displayMonitors = $resultMonitorFilters['displayMonitors']; +$selected_monitor_ids = $resultMonitorFilters['selected_monitor_ids']; $preference = ZM\User_Preference::find_one([ 'UserId'=>$user->Id(), @@ -211,10 +212,12 @@ if (isset($_REQUEST['fit'])) $fitMode = validCardinal($_REQUEST['fit']); -if (isset($_REQUEST['scale'])) - $defaultScale = validHtmlStr($_REQUEST['scale']); -else +if (isset($_REQUEST['scale'])) { + $defaultScale = validCardinal($_REQUEST['scale']); + if ($defaultScale > 1.1) $defaultScale = 1.0; +} else { $defaultScale = 1; +} $speeds = [0, 0.1, 0.25, 0.5, 0.75, 1.0, 1.5, 2, 3, 5, 10, 20, 50]; @@ -236,7 +239,7 @@ $initialDisplayInterval = 1000; if (isset($_REQUEST['displayinterval'])) - $initialDisplayInterval = validHtmlStr($_REQUEST['displayinterval']); + $initialDisplayInterval = validCardinal($_REQUEST['displayinterval']); $minTimeSecs = $maxTimeSecs = 0; if (isset($minTime) && isset($maxTime)) { @@ -271,17 +274,15 @@